@vheins/local-memory-mcp 0.1.44 → 0.2.0

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 (67) hide show
  1. package/dist/dashboard/dashboard.test.js +11 -3
  2. package/dist/dashboard/dashboard.test.js.map +1 -1
  3. package/dist/dashboard/public/app.js +541 -93
  4. package/dist/dashboard/public/index.html +1070 -718
  5. package/dist/dashboard/server.js +110 -16
  6. package/dist/dashboard/server.js.map +1 -1
  7. package/dist/e2e.test.js +25 -7
  8. package/dist/e2e.test.js.map +1 -1
  9. package/dist/prompts/registry.d.ts +12 -0
  10. package/dist/prompts/registry.d.ts.map +1 -1
  11. package/dist/prompts/registry.js +31 -0
  12. package/dist/prompts/registry.js.map +1 -1
  13. package/dist/resources/index.d.ts.map +1 -1
  14. package/dist/resources/index.js +23 -0
  15. package/dist/resources/index.js.map +1 -1
  16. package/dist/resources/index.test.js +2 -0
  17. package/dist/resources/index.test.js.map +1 -1
  18. package/dist/router.d.ts.map +1 -1
  19. package/dist/router.js +5 -0
  20. package/dist/router.js.map +1 -1
  21. package/dist/router.test.js +1 -1
  22. package/dist/router.test.js.map +1 -1
  23. package/dist/storage/sqlite.d.ts +6 -0
  24. package/dist/storage/sqlite.d.ts.map +1 -1
  25. package/dist/storage/sqlite.js +156 -21
  26. package/dist/storage/sqlite.js.map +1 -1
  27. package/dist/storage/sqlite.test.js +2 -0
  28. package/dist/storage/sqlite.test.js.map +1 -1
  29. package/dist/tasks.e2e.test.d.ts +2 -0
  30. package/dist/tasks.e2e.test.d.ts.map +1 -0
  31. package/dist/tasks.e2e.test.js +119 -0
  32. package/dist/tasks.e2e.test.js.map +1 -0
  33. package/dist/tools/memory.store.d.ts.map +1 -1
  34. package/dist/tools/memory.store.js +2 -0
  35. package/dist/tools/memory.store.js.map +1 -1
  36. package/dist/tools/schemas.d.ts +274 -0
  37. package/dist/tools/schemas.d.ts.map +1 -1
  38. package/dist/tools/schemas.js +128 -1
  39. package/dist/tools/schemas.js.map +1 -1
  40. package/dist/tools/task.manage.d.ts +16 -0
  41. package/dist/tools/task.manage.d.ts.map +1 -0
  42. package/dist/tools/task.manage.js +128 -0
  43. package/dist/tools/task.manage.js.map +1 -0
  44. package/dist/types.d.ts +22 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/utils/logger.d.ts.map +1 -1
  47. package/dist/utils/logger.js +47 -7
  48. package/dist/utils/logger.js.map +1 -1
  49. package/dist/v2-features.test.js +7 -1
  50. package/dist/v2-features.test.js.map +1 -1
  51. package/package.json +1 -1
  52. package/dist/search_memory_example.d.ts +0 -3
  53. package/dist/search_memory_example.d.ts.map +0 -1
  54. package/dist/search_memory_example.js +0 -56
  55. package/dist/search_memory_example.js.map +0 -1
  56. package/dist/server_robustness.test.d.ts +0 -2
  57. package/dist/server_robustness.test.d.ts.map +0 -1
  58. package/dist/server_robustness.test.js +0 -107
  59. package/dist/server_robustness.test.js.map +0 -1
  60. package/dist/store_memory_example.d.ts +0 -3
  61. package/dist/store_memory_example.d.ts.map +0 -1
  62. package/dist/store_memory_example.js +0 -69
  63. package/dist/store_memory_example.js.map +0 -1
  64. package/dist/test_quotes_client.d.ts +0 -3
  65. package/dist/test_quotes_client.d.ts.map +0 -1
  66. package/dist/test_quotes_client.js +0 -72
  67. package/dist/test_quotes_client.js.map +0 -1
@@ -23,6 +23,18 @@ let pinnedRepoOrder = [];
23
23
  let draggedPinnedRepo = null;
24
24
  let isGlobalFilterActive = false;
25
25
 
26
+ function syncStickyOffsets() {
27
+ const topBar = document.getElementById('mainTopBar');
28
+ const tabNav = document.querySelector('.sticky-tab-nav');
29
+ if (!topBar) return;
30
+
31
+ const topBarHeight = Math.ceil(topBar.getBoundingClientRect().height);
32
+ const tabNavHeight = tabNav ? Math.ceil(tabNav.getBoundingClientRect().height) : 0;
33
+
34
+ document.documentElement.style.setProperty('--dashboard-header-offset', `${topBarHeight}px`);
35
+ document.documentElement.style.setProperty('--dashboard-tab-offset', `${tabNavHeight}px`);
36
+ }
37
+
26
38
  async function loadRecentActions(page = recentActionsPage) {
27
39
  try {
28
40
  let url = `/api/recent-actions?page=${page}&pageSize=${recentActionsPageSize}`;
@@ -97,22 +109,22 @@ function getBubbleStyle(action) {
97
109
  bubble: 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-100',
98
110
  label: 'text-gray-500 dark:text-gray-400',
99
111
  meta: 'text-gray-400 dark:text-gray-500',
100
- align: 'items-end',
101
- tail: 'right-2 -bottom-1.5 border-l-gray-100 dark:border-l-gray-700',
112
+ align: 'items-start',
113
+ tail: 'left-2 -bottom-1.5 border-r-gray-100 dark:border-r-gray-700',
102
114
  },
103
115
  update: {
104
116
  bubble: 'bg-amber-100 dark:bg-amber-900 text-amber-900 dark:text-amber-100',
105
117
  label: 'text-amber-600 dark:text-amber-400',
106
118
  meta: 'text-amber-500 dark:text-amber-500',
107
- align: 'items-end',
108
- tail: 'right-2 -bottom-1.5 border-l-amber-100 dark:border-l-amber-900',
119
+ align: 'items-start',
120
+ tail: 'left-2 -bottom-1.5 border-r-amber-100 dark:border-r-amber-900',
109
121
  },
110
122
  delete: {
111
123
  bubble: 'bg-red-100 dark:bg-red-900 text-red-900 dark:text-red-100',
112
124
  label: 'text-red-500 dark:text-red-400',
113
125
  meta: 'text-red-400',
114
- align: 'items-end',
115
- tail: 'right-2 -bottom-1.5 border-l-red-100 dark:border-l-red-900',
126
+ align: 'items-start',
127
+ tail: 'left-2 -bottom-1.5 border-r-red-100 dark:border-r-red-900',
116
128
  },
117
129
  };
118
130
  return styles[action] || styles.search;
@@ -120,22 +132,28 @@ function getBubbleStyle(action) {
120
132
 
121
133
  function renderActionBubble(action) {
122
134
  const s = getBubbleStyle(action.action);
123
- const isRight = s.align === 'items-end';
135
+ const isRight = false; // Force left alignment
124
136
 
125
137
  // Main content line
126
138
  let mainText = '';
127
139
  let subText = '';
128
140
 
129
141
  if (action.action === 'search') {
130
- mainText = `"${action.query || ''}"`;
142
+ mainText = `🔍 "${action.query || ''}"`;
131
143
  subText = action.result_count != null ? `${action.result_count} result${action.result_count !== 1 ? 's' : ''} found` : '';
132
144
  } else {
133
- mainText = action.memory_title
134
- ? action.memory_title
135
- : action.memory_id ? action.memory_id.substring(0, 8) + '…' : '';
136
- const typeLabel = action.memory_type ? `[${action.memory_type}]` : '';
137
- const verb = { write: 'Stored', update: 'Updated', delete: 'Deleted', read: 'Read' }[action.action] || action.action;
138
- subText = [verb, typeLabel].filter(Boolean).join(' ');
145
+ if (action.task_id) {
146
+ mainText = action.task_title || action.task_code || action.task_id.substring(0, 8);
147
+ const verb = { write: '💾 Created Task', update: '🔄 Updated Task', delete: '🗑️ Deleted Task' }[action.action] || action.action;
148
+ subText = action.task_code ? `${verb} [${action.task_code}]` : verb;
149
+ } else {
150
+ mainText = action.memory_title
151
+ ? action.memory_title
152
+ : action.memory_id ? action.memory_id.substring(0, 8) + '…' : '—';
153
+ const typeLabel = action.memory_type ? `[${action.memory_type}]` : '';
154
+ const verb = { write: '💾 Stored', update: '🔄 Updated', delete: '🗑️ Deleted', read: '📖 Read' }[action.action] || action.action;
155
+ subText = [verb, typeLabel].filter(Boolean).join(' ');
156
+ }
139
157
  }
140
158
 
141
159
  const burst = action.burstCount > 1
@@ -144,14 +162,14 @@ function renderActionBubble(action) {
144
162
 
145
163
  return `
146
164
  <div class="flex flex-col ${s.align} mb-4">
147
- <div class="flex items-center gap-1.5 mb-1 px-1 ${isRight ? 'flex-row-reverse' : ''}">
148
- <div class="w-5 h-5 rounded-full ${s.align === 'items-start' ? s.bubble : 'bg-gray-300 dark:bg-gray-600'} flex items-center justify-center flex-shrink-0">
149
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">${getActionIcon(action.action)}</svg>
165
+ <div class="flex items-center gap-1.5 mb-1 px-1">
166
+ <div class="w-5 h-5 rounded-full ${s.bubble} flex items-center justify-center flex-shrink-0">
167
+ <svg class="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">${getActionIcon(action.action)}</svg>
150
168
  </div>
151
169
  <span class="text-xs font-semibold capitalize ${s.label.replace('text-', 'text-').replace('100','500').replace('200','500')}">${action.action}${burst}</span>
152
170
  </div>
153
- <div class="relative max-w-[85%]">
154
- <div class="${s.bubble} rounded-2xl ${isRight ? 'rounded-br-sm' : 'rounded-bl-sm'} px-3 py-2 shadow-sm">
171
+ <div class="relative max-w-[95%]">
172
+ <div class="${s.bubble} rounded-2xl rounded-bl-sm px-3 py-2 shadow-sm">
155
173
  <p class="text-sm font-medium leading-snug break-words">${mainText}</p>
156
174
  ${subText ? `<p class="text-xs mt-0.5 ${s.meta}">${subText}</p>` : ''}
157
175
  </div>
@@ -401,6 +419,7 @@ function renderRepoSidebar() {
401
419
  document.getElementById('repoSidebarList').innerHTML = renderGroups();
402
420
  const mobile = document.getElementById('repoSidebarListMobile');
403
421
  if (mobile) mobile.innerHTML = renderGroups();
422
+ syncStickyOffsets();
404
423
  }
405
424
 
406
425
  async function setCurrentRepo(repo) {
@@ -417,6 +436,7 @@ async function setCurrentRepo(repo) {
417
436
  loadMemories(),
418
437
  loadRecentActions(),
419
438
  ]);
439
+ syncStickyOffsets();
420
440
  }
421
441
 
422
442
  function openRepoSidebarDrawer() {
@@ -517,6 +537,11 @@ async function checkStatus() {
517
537
  dbPathLabel.title = data.dbPath;
518
538
  }
519
539
 
540
+ const appVersion = document.getElementById('appVersion');
541
+ if (appVersion && data.version) {
542
+ appVersion.textContent = `v${data.version}`;
543
+ }
544
+
520
545
  const summary = document.getElementById('memorySummaryLabel');
521
546
  if (summary) {
522
547
  summary.textContent = `${data.memoryCount || 0} memories indexed`;
@@ -561,6 +586,9 @@ async function loadStats() {
561
586
  try {
562
587
  const url = currentRepo ? `/api/stats?repo=${encodeURIComponent(currentRepo)}` : '/api/stats';
563
588
  const response = await fetch(url);
589
+ if (!response.ok) {
590
+ throw new Error(`Stats request failed with ${response.status}`);
591
+ }
564
592
  const data = await response.json();
565
593
 
566
594
  document.getElementById('totalCount').textContent = data.total;
@@ -576,6 +604,7 @@ async function loadStats() {
576
604
  updateTimeSeriesChart(data.timeSeries || {});
577
605
  updateScatterChart(data.scatterData || []);
578
606
  updateTopMemoriesChart(data.topMemories);
607
+ syncStickyOffsets();
579
608
  } catch (err) {
580
609
  console.error('Failed to load stats:', err);
581
610
  }
@@ -583,38 +612,58 @@ async function loadStats() {
583
612
 
584
613
  function updateTypeChart(byType) {
585
614
  const ctx = document.getElementById('typeChart');
586
- if (!window.Chart) {
587
- ctx.parentElement.innerHTML = '<div class="p-8 text-center text-gray-500">Chart.js not available</div>';
588
- return;
589
- }
615
+ if (!window.Chart || !ctx) return;
590
616
  if (charts.typeChart) charts.typeChart.destroy();
591
617
 
592
- const types = Object.keys(byType || {});
593
- const counts = Object.values(byType || {});
618
+ const isDark = document.documentElement.classList.contains('dark');
619
+ const counts = [
620
+ byType?.decision || 0,
621
+ byType?.mistake || 0,
622
+ byType?.code_fact || 0,
623
+ byType?.pattern || 0
624
+ ];
594
625
 
595
626
  charts.typeChart = new Chart(ctx, {
596
- type: 'pie',
627
+ type: 'doughnut',
597
628
  data: {
598
- labels: types.map(t => t.replace('_', ' ')),
629
+ labels: ['Decision', 'Mistake', 'Code Fact', 'Pattern'],
599
630
  datasets: [{
600
631
  data: counts,
601
- backgroundColor: ['#38bdf8', '#fb7185', '#a78bfa', '#34d399'],
602
- borderColor: 'rgba(255,255,255,0.72)',
603
- borderWidth: 2
632
+ backgroundColor: ['#fb7185', '#c084fc', '#38bdf8', '#34d399'],
633
+ borderWidth: 2,
634
+ borderColor: isDark ? '#1e293b' : '#ffffff',
635
+ hoverOffset: 12
604
636
  }]
605
637
  },
606
- options: { responsive: true, maintainAspectRatio: true }
638
+ options: {
639
+ responsive: true,
640
+ maintainAspectRatio: false,
641
+ cutout: '68%',
642
+ plugins: {
643
+ legend: { display: false },
644
+ tooltip: {
645
+ backgroundColor: isDark ? '#1e293b' : '#ffffff',
646
+ titleColor: isDark ? '#f8fafc' : '#1e293b',
647
+ bodyColor: isDark ? '#94a3b8' : '#64748b',
648
+ borderColor: isDark ? '#334155' : '#e2e8f0',
649
+ borderWidth: 1,
650
+ padding: 10,
651
+ cornerRadius: 10
652
+ }
653
+ }
654
+ }
607
655
  });
608
656
  }
609
657
 
610
658
  function updateTopMemoriesChart(memories = []) {
611
659
  const ctx = document.getElementById('topMemoriesChart');
612
660
  if (!window.Chart) {
613
- ctx.parentElement.innerHTML = '<div class="p-8 text-center text-gray-500">Chart.js not available</div>';
661
+ ctx.parentElement.innerHTML = '<div class="p-4 text-center text-gray-500 text-xs">Chart.js not available</div>';
614
662
  return;
615
663
  }
616
664
  if (charts.topMemoriesChart) charts.topMemoriesChart.destroy();
617
665
 
666
+ const isDark = document.documentElement.classList.contains('dark');
618
667
  charts.topMemoriesChart = new Chart(ctx, {
619
668
  type: 'bar',
620
669
  data: {
@@ -622,59 +671,263 @@ function updateTopMemoriesChart(memories = []) {
622
671
  datasets: [{
623
672
  label: 'Hit Count',
624
673
  data: memories.map(m => m.hit_count || m.importance),
625
- backgroundColor: ['#38bdf8', '#60a5fa', '#22d3ee', '#7dd3fc', '#93c5fd', '#67e8f9', '#38bdf8', '#60a5fa', '#22d3ee', '#7dd3fc'],
626
- borderRadius: 10
674
+ backgroundColor: isDark
675
+ ? 'rgba(56,189,248,0.45)'
676
+ : 'rgba(37,99,235,0.55)',
677
+ borderColor: isDark ? '#38bdf8' : '#2563eb',
678
+ borderWidth: 1,
679
+ borderRadius: 6,
680
+ borderSkipped: false
627
681
  }]
628
682
  },
629
- options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: true } } }
683
+ options: {
684
+ responsive: true,
685
+ maintainAspectRatio: false,
686
+ plugins: {
687
+ legend: { display: false },
688
+ tooltip: {
689
+ backgroundColor: isDark ? '#1e293b' : '#ffffff',
690
+ titleColor: isDark ? '#f8fafc' : '#1e293b',
691
+ bodyColor: isDark ? '#94a3b8' : '#64748b',
692
+ borderColor: isDark ? '#334155' : '#e2e8f0',
693
+ borderWidth: 1,
694
+ padding: 8,
695
+ cornerRadius: 8
696
+ }
697
+ },
698
+ scales: {
699
+ y: {
700
+ beginAtZero: true,
701
+ grid: { color: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.04)', drawBorder: false },
702
+ ticks: { color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 } }
703
+ },
704
+ x: {
705
+ grid: { display: false },
706
+ ticks: { color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 } }
707
+ }
708
+ }
709
+ }
630
710
  });
631
711
  }
632
712
 
633
713
  function updateTimeSeriesChart(timeSeries) {
634
714
  const ctx = document.getElementById('timeSeriesChart');
635
- if (!window.Chart) return;
715
+ if (!window.Chart || !ctx) return;
636
716
  if (charts.timeSeriesChart) charts.timeSeriesChart.destroy();
637
717
 
638
- const labels = Object.keys(timeSeries).slice(-14);
639
- const data = Object.values(timeSeries).slice(-14);
718
+ const labels = Object.keys(timeSeries);
719
+ const creationData = labels.map(k => (typeof timeSeries[k] === 'object' ? timeSeries[k].write : timeSeries[k]) || 0);
720
+ const readData = labels.map(k => (typeof timeSeries[k] === 'object' ? timeSeries[k].read : 0) || 0);
721
+ const searchData = labels.map(k => (typeof timeSeries[k] === 'object' ? timeSeries[k].search : 0) || 0);
722
+
723
+ const isDark = document.documentElement.classList.contains('dark');
640
724
 
641
725
  charts.timeSeriesChart = new Chart(ctx, {
642
726
  type: 'line',
643
727
  data: {
644
- labels,
645
- datasets: [{
646
- label: 'Memories Created',
647
- data,
648
- borderColor: '#22d3ee',
649
- backgroundColor: 'rgba(34, 211, 238, 0.16)',
650
- pointBackgroundColor: '#7dd3fc',
651
- pointBorderColor: '#e0f2fe',
652
- fill: true,
653
- tension: 0.35
654
- }]
728
+ labels: labels.map(l => l.split('-').slice(1).join('/')),
729
+ datasets: [
730
+ {
731
+ label: 'Created',
732
+ data: creationData,
733
+ borderColor: '#22d3ee',
734
+ backgroundColor: 'rgba(34, 211, 238, 0.1)',
735
+ borderWidth: 2,
736
+ pointRadius: 3,
737
+ tension: 0.4,
738
+ fill: true
739
+ },
740
+ {
741
+ label: 'Read',
742
+ data: readData,
743
+ borderColor: '#10b981',
744
+ backgroundColor: 'transparent',
745
+ borderWidth: 2,
746
+ pointRadius: 3,
747
+ tension: 0.4,
748
+ borderDash: [5, 5]
749
+ },
750
+ {
751
+ label: 'Search',
752
+ data: searchData,
753
+ borderColor: '#6366f1',
754
+ backgroundColor: 'transparent',
755
+ borderWidth: 2,
756
+ pointRadius: 3,
757
+ tension: 0.4,
758
+ borderDash: [2, 2]
759
+ }
760
+ ]
655
761
  },
656
- options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
762
+ options: {
763
+ responsive: true,
764
+ maintainAspectRatio: false,
765
+ plugins: {
766
+ legend: {
767
+ position: 'top',
768
+ align: 'end',
769
+ labels: {
770
+ color: isDark ? '#94a3b8' : '#64748b',
771
+ usePointStyle: true,
772
+ boxWidth: 6,
773
+ font: { size: 10, weight: 'bold' }
774
+ }
775
+ }
776
+ },
777
+ scales: {
778
+ y: {
779
+ beginAtZero: true,
780
+ grid: { color: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)', drawBorder: false },
781
+ ticks: { color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 }, stepSize: 1 }
782
+ },
783
+ x: {
784
+ grid: { display: false },
785
+ ticks: { color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 }, maxRotation: 0 }
786
+ }
787
+ }
788
+ }
657
789
  });
658
790
  }
659
791
 
792
+ function showAddMemoryModal() {
793
+ if (!currentRepo) {
794
+ showToast('Please select a repository first', 'error');
795
+ return;
796
+ }
797
+ document.getElementById('addMemoryModal').classList.remove('hidden');
798
+ document.body.classList.add('drawer-open');
799
+ }
800
+
801
+ function hideAddMemoryModal() {
802
+ document.getElementById('addMemoryModal').classList.add('hidden');
803
+ document.body.classList.remove('drawer-open');
804
+ document.getElementById('addMemoryForm').reset();
805
+ }
806
+
807
+ function showAddTaskModal() {
808
+ if (!currentRepo) {
809
+ showToast('Please select a repository first', 'error');
810
+ return;
811
+ }
812
+ document.getElementById('addTaskModal').classList.remove('hidden');
813
+ document.body.classList.add('drawer-open');
814
+ }
815
+
816
+ function hideAddTaskModal() {
817
+ document.getElementById('addTaskModal').classList.add('hidden');
818
+ document.body.classList.remove('drawer-open');
819
+ document.getElementById('addTaskForm').reset();
820
+ }
821
+
822
+ async function handleMemorySubmit(event) {
823
+ event.preventDefault();
824
+ const formData = new FormData(event.target);
825
+ const data = Object.fromEntries(formData.entries());
826
+ data.repo = currentRepo;
827
+ data.is_global = event.target.is_global.checked;
828
+ // agent and model are already in data from FormData entries
829
+
830
+ try {
831
+ const res = await fetch('/api/memories', {
832
+ method: 'POST',
833
+ headers: { 'Content-Type': 'application/json' },
834
+ body: JSON.stringify(data)
835
+ });
836
+ if (res.ok) {
837
+ showToast('Memory added successfully', 'success');
838
+ hideAddMemoryModal();
839
+ loadData();
840
+ } else {
841
+ const err = await res.json();
842
+ showToast(err.error || 'Failed to add memory', 'error');
843
+ }
844
+ } catch (err) {
845
+ showToast('Network error', 'error');
846
+ }
847
+ }
848
+
849
+ async function handleTaskSubmit(event) {
850
+ event.preventDefault();
851
+ const formData = new FormData(event.target);
852
+ const data = Object.fromEntries(formData.entries());
853
+ data.repo = currentRepo;
854
+
855
+ try {
856
+ const res = await fetch('/api/tasks', {
857
+ method: 'POST',
858
+ headers: { 'Content-Type': 'application/json' },
859
+ body: JSON.stringify(data)
860
+ });
861
+ if (res.ok) {
862
+ showToast('Task created successfully', 'success');
863
+ hideAddTaskModal();
864
+ loadTasks();
865
+ loadRecentActions();
866
+ } else {
867
+ const err = await res.json();
868
+ showToast(err.error || 'Failed to create task', 'error');
869
+ }
870
+ } catch (err) {
871
+ showToast('Network error', 'error');
872
+ }
873
+ }
874
+
875
+ window.showAddMemoryModal = showAddMemoryModal;
876
+ window.hideAddMemoryModal = hideAddMemoryModal;
877
+ window.showAddTaskModal = showAddTaskModal;
878
+ window.hideAddTaskModal = hideAddTaskModal;
879
+ window.handleMemorySubmit = handleMemorySubmit;
880
+ window.handleTaskSubmit = handleTaskSubmit;
881
+
660
882
  function updateScatterChart(scatterData) {
661
883
  const ctx = document.getElementById('scatterChart');
662
884
  if (!window.Chart) return;
663
885
  if (charts.scatterChart) charts.scatterChart.destroy();
664
886
 
887
+ const isDark = document.documentElement.classList.contains('dark');
665
888
  charts.scatterChart = new Chart(ctx, {
666
889
  type: 'scatter',
667
890
  data: {
668
891
  datasets: [{
669
892
  label: 'Memories',
670
893
  data: scatterData,
671
- backgroundColor: 'rgba(96, 165, 250, 0.85)',
672
- borderColor: '#a78bfa',
673
- pointRadius: 4.5,
674
- pointHoverRadius: 6
894
+ backgroundColor: isDark ? 'rgba(129,140,248,0.55)' : 'rgba(96,165,250,0.7)',
895
+ borderColor: isDark ? '#818cf8' : '#6366f1',
896
+ borderWidth: 1,
897
+ pointRadius: 5,
898
+ pointHoverRadius: 7
675
899
  }]
676
900
  },
677
- options: { responsive: true, maintainAspectRatio: true, scales: { x: { title: { display: true, text: 'Importance' }, min: 0, max: 6 }, y: { title: { display: true, text: 'Hit Count' }, beginAtZero: true } } }
901
+ options: {
902
+ responsive: true,
903
+ maintainAspectRatio: false,
904
+ plugins: {
905
+ legend: { display: false },
906
+ tooltip: {
907
+ backgroundColor: isDark ? '#1e293b' : '#ffffff',
908
+ titleColor: isDark ? '#f8fafc' : '#1e293b',
909
+ bodyColor: isDark ? '#94a3b8' : '#64748b',
910
+ borderColor: isDark ? '#334155' : '#e2e8f0',
911
+ borderWidth: 1,
912
+ padding: 8,
913
+ cornerRadius: 8
914
+ }
915
+ },
916
+ scales: {
917
+ x: {
918
+ title: { display: true, text: 'Importance', color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 } },
919
+ min: 0, max: 6,
920
+ grid: { color: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.04)' },
921
+ ticks: { color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 } }
922
+ },
923
+ y: {
924
+ title: { display: true, text: 'Hit Count', color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 } },
925
+ beginAtZero: true,
926
+ grid: { color: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.04)' },
927
+ ticks: { color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 } }
928
+ }
929
+ }
930
+ }
678
931
  });
679
932
  }
680
933
 
@@ -721,6 +974,7 @@ function renderTableSkeleton() {
721
974
  <tr class="border-b-2 border-gray-200 dark:border-gray-700">
722
975
  <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold"></th>
723
976
  <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Memory</th>
977
+ <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Source</th>
724
978
  <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Type</th>
725
979
  <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Priority</th>
726
980
  <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Usage</th>
@@ -733,6 +987,7 @@ function renderTableSkeleton() {
733
987
  <tr class="border-b border-gray-100 dark:border-gray-700">
734
988
  <td class="p-3"><div class="skeleton h-4 w-4"></div></td>
735
989
  <td class="p-3"><div class="skeleton h-4 w-52 mb-2"></div><div class="skeleton h-3 w-36"></div></td>
990
+ <td class="p-3"><div class="skeleton h-4 w-20 mb-2"></div><div class="skeleton h-3 w-16"></div></td>
736
991
  <td class="p-3"><div class="skeleton h-6 w-20"></div></td>
737
992
  <td class="p-3"><div class="skeleton h-6 w-12 mb-2"></div><div class="skeleton h-3 w-12"></div></td>
738
993
  <td class="p-3"><div class="skeleton h-4 w-16 mb-2"></div><div class="skeleton h-3 w-14"></div></td>
@@ -752,6 +1007,24 @@ function escapeHtml(text) {
752
1007
  return div.innerHTML;
753
1008
  }
754
1009
 
1010
+ function renderMarkdown(text) {
1011
+ if (!text) return '';
1012
+ if (typeof marked === 'undefined') {
1013
+ // Fallback if marked.js didn't load
1014
+ const div = document.createElement('div');
1015
+ div.textContent = text;
1016
+ return `<pre style="white-space:pre-wrap;font-size:0.85rem;line-height:1.7">${div.innerHTML}</pre>`;
1017
+ }
1018
+ try {
1019
+ marked.setOptions({ breaks: true, gfm: true });
1020
+ return marked.parse(text);
1021
+ } catch (e) {
1022
+ const div = document.createElement('div');
1023
+ div.textContent = text;
1024
+ return `<pre style="white-space:pre-wrap">${div.innerHTML}</pre>`;
1025
+ }
1026
+ }
1027
+
755
1028
  function formatDate(dateStr) {
756
1029
  const date = new Date(dateStr);
757
1030
  const now = new Date();
@@ -786,7 +1059,9 @@ function renderTable(memories) {
786
1059
  <tr class="border-b-2 border-gray-200 dark:border-gray-700">
787
1060
  <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold"><input type="checkbox" id="selectAll" onchange="toggleSelectAll()"></th>
788
1061
  <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold cursor-pointer" onclick="sortTable('title')" data-sort="title">Memory <span class="sort-icon"></span></th>
1062
+ <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold cursor-pointer" onclick="sortTable('agent')" data-sort="agent">Source <span class="sort-icon"></span></th>
789
1063
  <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold cursor-pointer" onclick="sortTable('type')" data-sort="type">Type <span class="sort-icon"></span></th>
1064
+
790
1065
  <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold cursor-pointer" onclick="sortTable('importance')" data-sort="importance">Priority <span class="sort-icon"></span></th>
791
1066
  <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold cursor-pointer" onclick="sortTable('hit_count')" data-sort="hit_count">Usage <span class="sort-icon"></span></th>
792
1067
  <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold cursor-pointer" onclick="sortTable('created_at')" data-sort="created_at">Freshness <span class="sort-icon"></span></th>
@@ -811,6 +1086,12 @@ function renderTable(memories) {
811
1086
  <span>${m.scope?.repo || 'Unknown repo'}</span>
812
1087
  </div>
813
1088
  </td>
1089
+ <td class="p-3">
1090
+ <div class="flex flex-col">
1091
+ <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>
1092
+ <span class="text-[9px] text-gray-400 dark:text-gray-500 truncate max-w-[100px]" title="${m.model || 'unknown'}">${m.model || 'unknown'}</span>
1093
+ </div>
1094
+ </td>
814
1095
  <td class="p-3"><span class="table-chip type-${m.type}">${formatTypeLabel(m.type)}</span></td>
815
1096
  <td class="p-3">
816
1097
  <div class="metric-badge ${getImportanceBadgeClass(m.importance)}">${m.importance}/5</div>
@@ -825,9 +1106,9 @@ function renderTable(memories) {
825
1106
  <div class="mt-1 text-xs text-gray-500 dark:text-gray-400">Updated ${formatDate(m.updated_at)}</div>
826
1107
  </td>
827
1108
  <td class="p-3 sticky-actions">
828
- <div class="flex flex-wrap gap-2">
829
- <button onclick="openDrawer('${m.id}')" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium">Open</button>
830
- <button onclick="startInlineEdit('${m.id}')" class="px-3 py-1.5 bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200 rounded hover:bg-amber-200 dark:hover:bg-amber-900/60 text-xs font-medium">Edit</button>
1109
+ <div class="flex flex-wrap gap-1.5">
1110
+ <button onclick="openDrawer('${m.id}')" class="btn-open">Open</button>
1111
+ <button onclick="startInlineEdit('${m.id}')" class="btn-edit-light">Edit</button>
831
1112
  </div>
832
1113
  </td>
833
1114
  </tr>
@@ -910,11 +1191,11 @@ function getImportanceLabel(importance) {
910
1191
  }
911
1192
 
912
1193
  function getImportanceBadgeClass(importance) {
913
- if (importance >= 5) return 'bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-200';
914
- if (importance >= 4) return 'bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-200';
915
- if (importance >= 3) return 'bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-200';
916
- if (importance >= 2) return 'bg-sky-100 text-sky-700 dark:bg-sky-900/50 dark:text-sky-200';
917
- return 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200';
1194
+ if (importance >= 5) return 'bg-gradient-to-r from-red-500/10 to-rose-500/10 text-red-700 dark:text-red-300 border-red-400/30';
1195
+ if (importance >= 4) return 'bg-gradient-to-r from-orange-500/10 to-amber-500/10 text-orange-700 dark:text-orange-300 border-orange-400/30';
1196
+ if (importance >= 3) return 'bg-gradient-to-r from-amber-400/10 to-yellow-400/10 text-amber-700 dark:text-amber-300 border-amber-400/30';
1197
+ if (importance >= 2) return 'bg-gradient-to-r from-sky-400/10 to-blue-400/10 text-sky-700 dark:text-sky-300 border-sky-400/30';
1198
+ return 'bg-gradient-to-r from-slate-300/10 to-gray-300/10 text-slate-600 dark:text-slate-400 border-slate-300/30';
918
1199
  }
919
1200
 
920
1201
  function formatUsageCount(hitCount) {
@@ -994,27 +1275,34 @@ function renderDetailPanel(data) {
994
1275
  <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-2">Summary</div>
995
1276
  <p class="text-sm leading-6 text-gray-700 dark:text-gray-300">${escapeHtml(getContentPreview(data))}</p>
996
1277
  </div>
997
- <div class="grid gap-4 md:grid-cols-2">
1278
+ <div class="grid gap-4 md:grid-cols-3">
998
1279
  <div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
999
1280
  <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">Memory Info</div>
1000
1281
  <div class="space-y-2 text-sm">
1001
1282
  <div><strong>Type:</strong> ${formatTypeLabel(data.type)}</div>
1002
- <div><strong>ID:</strong> <span class="font-mono">${data.id}</span></div>
1003
- <div><strong>Repo:</strong> ${escapeHtml(data.scope?.repo || 'N/A')}</div>
1004
- <div><strong>Priority:</strong> ${data.importance}/5 (${getImportanceLabel(data.importance)})</div>
1283
+ <div><strong>ID:</strong> <span class="font-mono text-[10px]">${data.id}</span></div>
1284
+ <div><strong>Priority:</strong> ${data.importance}/5</div>
1005
1285
  <div><strong>Status:</strong> <span class="capitalize px-1.5 py-0.5 rounded ${data.status === 'active' ? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300' : 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400'}">${data.status}</span></div>
1006
1286
  </div>
1007
1287
  </div>
1288
+ <div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
1289
+ <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">Source Info</div>
1290
+ <div class="space-y-2 text-sm">
1291
+ <div><strong>Agent:</strong> ${escapeHtml(data.agent || 'unknown')}</div>
1292
+ <div><strong>Model:</strong> ${escapeHtml(data.model || 'unknown')}</div>
1293
+ <div><strong>Repo:</strong> ${escapeHtml(data.scope?.repo || 'N/A')}</div>
1294
+ </div>
1295
+ </div>
1008
1296
  <div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
1009
1297
  <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">Usage</div>
1010
1298
  <div class="space-y-2 text-sm">
1011
1299
  <div><strong>Hit Count:</strong> ${data.hit_count || 0}</div>
1012
- <div><strong>Recall Count:</strong> ${data.recall_count || 0}</div>
1013
1300
  <div><strong>Recall Rate:</strong> ${formatRecallRate(data.recall_rate)}</div>
1014
- <div><strong>Last Used:</strong> ${data.last_used_at ? new Date(data.last_used_at).toLocaleString() : 'Never'}</div>
1301
+ <div><strong>Last Used:</strong> ${data.last_used_at ? new Date(data.last_used_at).toLocaleDateString() : 'Never'}</div>
1015
1302
  </div>
1016
1303
  </div>
1017
1304
  </div>
1305
+
1018
1306
  ${data.supersedes ? `
1019
1307
  <div class="rounded-xl border border-blue-200 dark:border-blue-900 bg-blue-50 dark:bg-blue-900/20 p-4">
1020
1308
  <div class="text-xs uppercase tracking-wide text-blue-500 dark:text-blue-400 mb-2">Supersedes</div>
@@ -1032,9 +1320,15 @@ function renderDetailPanel(data) {
1032
1320
  <div><strong>Expires:</strong> ${data.expires_at ? new Date(data.expires_at).toLocaleString() : 'Never'}</div>
1033
1321
  </div>
1034
1322
  </div>
1035
- <div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
1036
- <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">Full Content</div>
1037
- <pre class="whitespace-pre-wrap text-sm leading-6">${escapeHtml(data.content)}</pre>
1323
+ <div class="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
1324
+ <div class="flex items-center justify-between px-4 py-2.5 bg-gray-50 dark:bg-gray-900/60 border-b border-gray-200 dark:border-gray-700">
1325
+ <div class="text-xs uppercase tracking-wide font-bold text-gray-500 dark:text-gray-400">Full Content</div>
1326
+ <span class="text-[10px] text-gray-400 dark:text-gray-500 flex items-center gap-1">
1327
+ <svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg>
1328
+ Markdown
1329
+ </span>
1330
+ </div>
1331
+ <div class="p-4 md:p-5 markdown-body">${renderMarkdown(data.content)}</div>
1038
1332
  </div>
1039
1333
  <div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
1040
1334
  <div class="flex items-center justify-between mb-3">
@@ -1083,6 +1377,16 @@ async function openDrawer(id) {
1083
1377
  `;
1084
1378
  document.getElementById('memoryDrawer').classList.remove('hidden');
1085
1379
  document.body.classList.add('drawer-open');
1380
+
1381
+ // Trigger slide-in animation
1382
+ setTimeout(() => {
1383
+ const aside = document.getElementById('drawerAside');
1384
+ if (aside) {
1385
+ aside.classList.remove('translate-x-full');
1386
+ aside.classList.add('translate-x-0');
1387
+ }
1388
+ }, 10);
1389
+
1086
1390
  const response = await fetch(`/api/memories/${id}?repo=${encodeURIComponent(currentRepo)}`);
1087
1391
  const data = await response.json();
1088
1392
  if (!response.ok) throw new Error(data.error || 'Failed to load memory');
@@ -1095,10 +1399,20 @@ async function openDrawer(id) {
1095
1399
  }
1096
1400
 
1097
1401
  function closeDrawer() {
1098
- document.getElementById('memoryDrawer').classList.add('hidden');
1099
- if (!document.getElementById('repoSidebarDrawer') || document.getElementById('repoSidebarDrawer').classList.contains('hidden')) {
1100
- document.body.classList.remove('drawer-open');
1402
+ const aside = document.getElementById('drawerAside');
1403
+ if (aside) {
1404
+ aside.classList.remove('translate-x-0');
1405
+ aside.classList.add('translate-x-full');
1101
1406
  }
1407
+
1408
+ // Delay hiding the container until animation completes
1409
+ setTimeout(() => {
1410
+ document.getElementById('memoryDrawer').classList.add('hidden');
1411
+ if (!document.getElementById('repoSidebarDrawer') || document.getElementById('repoSidebarDrawer').classList.contains('hidden')) {
1412
+ document.body.classList.remove('drawer-open');
1413
+ }
1414
+ }, 500);
1415
+
1102
1416
  activeEditMemoryId = null;
1103
1417
  currentDrawerMemoryId = null;
1104
1418
  }
@@ -1265,25 +1579,157 @@ async function loadData() {
1265
1579
  await Promise.all([
1266
1580
  loadStats(),
1267
1581
  loadMemories(),
1582
+ loadTasks(),
1268
1583
  checkStatus(),
1269
1584
  loadRecentActions(),
1270
1585
  ]);
1271
1586
  }
1272
1587
 
1273
- document.getElementById('repoSearchInput').addEventListener('input', () => {
1588
+ let currentTasks = [];
1589
+
1590
+ async function loadTasks() {
1591
+ if (!currentRepo) return;
1592
+ try {
1593
+ const response = await fetch(`/api/tasks?repo=${encodeURIComponent(currentRepo)}`);
1594
+ const data = await response.json();
1595
+ currentTasks = data.tasks || [];
1596
+ renderTasks();
1597
+ } catch (err) {
1598
+ console.error('Failed to load tasks:', err);
1599
+ }
1600
+ }
1601
+
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);
1616
+ }
1617
+
1618
+ function renderTaskColumn(id, tasks) {
1619
+ const container = document.getElementById(id);
1620
+ if (!container) return;
1621
+
1622
+ if (!tasks || tasks.length === 0) {
1623
+ 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
+ return;
1625
+ }
1626
+
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">
1629
+ <div class="flex items-center justify-between mb-2">
1630
+ <div class="flex items-center gap-2">
1631
+ <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
+ <span class="text-[10px] font-bold uppercase tracking-wider text-gray-400">${t.phase}</span>
1633
+ </div>
1634
+ <div class="flex items-center gap-1">
1635
+ ${t.priority >= 4 ? '<span class="w-2 h-2 rounded-full bg-red-500 animate-pulse"></span>' : ''}
1636
+ <span class="text-[10px] font-bold ${getPriorityColor(t.priority)}">P${t.priority}</span>
1637
+ </div>
1638
+ </div>
1639
+ <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>
1641
+ ${t.depends_on ? `
1642
+ <div class="mt-2 pt-2 border-t border-gray-50 dark:border-gray-600 flex items-center gap-1.5">
1643
+ <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>
1644
+ <span class="text-[10px] font-medium text-amber-600 dark:text-amber-400">Depends on: ${t.depends_on.substring(0, 8)}</span>
1645
+ </div>
1646
+ ` : ''}
1647
+ <div class="mt-3 flex items-center justify-between">
1648
+ <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>
1650
+ </div>
1651
+ </div>
1652
+ `).join('');
1653
+ }
1654
+
1655
+ function getPriorityColor(p) {
1656
+ if (p >= 5) return 'text-red-600 dark:text-red-400';
1657
+ if (p >= 4) return 'text-orange-600 dark:text-orange-400';
1658
+ if (p >= 3) return 'text-amber-600 dark:text-amber-400';
1659
+ return 'text-gray-500 dark:text-gray-400';
1660
+ }
1661
+
1662
+ let currentTab = localStorage.getItem('activeTab') || 'dashboard';
1663
+
1664
+ function switchTab(tab) {
1665
+ const dashTab = document.getElementById('dashboardTabBtn');
1666
+ const memTab = document.getElementById('memoriesTabBtn');
1667
+ const taskTab = document.getElementById('tasksTabBtn');
1668
+ const indicator = document.getElementById('tabIndicator');
1669
+
1670
+ const dashContent = document.getElementById('dashboardContent');
1671
+ const memContent = document.getElementById('memoriesContent');
1672
+ const taskContent = document.getElementById('tasksContent');
1673
+
1674
+ currentTab = tab;
1675
+ localStorage.setItem('activeTab', tab);
1676
+
1677
+ const tabs = [dashTab, memTab, taskTab];
1678
+ const contents = [dashContent, memContent, taskContent];
1679
+ const targetId = tab + 'TabBtn';
1680
+ const targetContentId = tab + 'Content';
1681
+
1682
+ // Update indicator
1683
+ if (indicator) {
1684
+ 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%)';
1687
+ }
1688
+
1689
+ // Update button states
1690
+ tabs.forEach(t => {
1691
+ if (t) {
1692
+ if (t.id === targetId) {
1693
+ t.classList.add('text-white');
1694
+ t.classList.remove('text-gray-500', 'dark:text-gray-400');
1695
+ } else {
1696
+ t.classList.remove('text-white');
1697
+ t.classList.add('text-gray-500', 'dark:text-gray-400');
1698
+ }
1699
+ }
1700
+ });
1701
+
1702
+ // Update content visibility
1703
+ contents.forEach(c => {
1704
+ if (c) {
1705
+ if (c.id === targetContentId) {
1706
+ c.classList.remove('hidden');
1707
+ } else {
1708
+ c.classList.add('hidden');
1709
+ }
1710
+ }
1711
+ });
1712
+
1713
+ // Trigger data loads if needed
1714
+ if (tab === 'tasks') loadTasks();
1715
+ if (tab === 'dashboard') loadStats();
1716
+ if (tab === 'memories') loadMemories();
1717
+ }
1718
+
1719
+ window.loadTasks = loadTasks;
1720
+ window.switchTab = switchTab;
1721
+ window.charts = charts;
1722
+
1723
+ const safeAddEventListener = (id, event, handler) => {
1724
+ const el = document.getElementById(id);
1725
+ if (el) el.addEventListener(event, handler);
1726
+ };
1727
+
1728
+ safeAddEventListener('repoSearchInput', 'input', () => {
1274
1729
  renderRepoSidebar();
1275
1730
  });
1276
1731
 
1277
- window.setCurrentRepo = setCurrentRepo;
1278
- window.closeRepoSidebarDrawer = closeRepoSidebarDrawer;
1279
- window.togglePinnedRepo = togglePinnedRepo;
1280
- window.startPinnedRepoDrag = startPinnedRepoDrag;
1281
- window.overPinnedRepoDrag = overPinnedRepoDrag;
1282
- window.leavePinnedRepoDrag = leavePinnedRepoDrag;
1283
- window.dropPinnedRepo = dropPinnedRepo;
1284
- window.endPinnedRepoDrag = endPinnedRepoDrag;
1285
-
1286
- document.getElementById('repoSearchInput').addEventListener('keydown', (e) => {
1732
+ safeAddEventListener('repoSearchInput', 'keydown', (e) => {
1287
1733
  if (e.key === 'Enter') {
1288
1734
  const firstMatch = availableRepos.find((item) => item.repo.toLowerCase().includes(e.target.value.trim().toLowerCase()));
1289
1735
  if (firstMatch) {
@@ -1292,11 +1738,11 @@ document.getElementById('repoSearchInput').addEventListener('keydown', (e) => {
1292
1738
  }
1293
1739
  });
1294
1740
 
1295
- document.getElementById('repoSearchInputMobile').addEventListener('input', () => {
1741
+ safeAddEventListener('repoSearchInputMobile', 'input', () => {
1296
1742
  renderRepoSidebar();
1297
1743
  });
1298
1744
 
1299
- document.getElementById('repoSearchInputMobile').addEventListener('keydown', (e) => {
1745
+ safeAddEventListener('repoSearchInputMobile', 'keydown', (e) => {
1300
1746
  if (e.key === 'Enter') {
1301
1747
  const firstMatch = availableRepos.find((item) => item.repo.toLowerCase().includes(e.target.value.trim().toLowerCase()));
1302
1748
  if (firstMatch) {
@@ -1305,26 +1751,28 @@ document.getElementById('repoSearchInputMobile').addEventListener('keydown', (e)
1305
1751
  }
1306
1752
  });
1307
1753
 
1308
- document.getElementById('repoNavToggle').addEventListener('click', () => {
1754
+ safeAddEventListener('repoNavToggle', 'click', () => {
1309
1755
  openRepoSidebarDrawer();
1310
1756
  });
1311
1757
 
1312
- document.getElementById('repoSidebarCollapseToggle').addEventListener('click', () => {
1758
+ safeAddEventListener('repoSidebarCollapseToggle', 'click', () => {
1313
1759
  toggleRepoSidebarCollapse();
1314
1760
  });
1315
1761
 
1316
- document.getElementById('repoCollapsedSummaryButton').addEventListener('click', () => {
1762
+ safeAddEventListener('repoCollapsedSummaryButton', 'click', () => {
1317
1763
  if (isRepoSidebarCollapsed) {
1318
1764
  toggleRepoSidebarCollapse();
1319
1765
  }
1320
1766
  });
1321
1767
 
1768
+ window.addEventListener('resize', syncStickyOffsets);
1769
+
1322
1770
  initTheme();
1323
1771
  initRepoSidebarState();
1324
1772
  initPinnedRepos();
1325
1773
  renderRecentActions();
1326
1774
  loadData();
1775
+ switchTab(currentTab);
1776
+ syncStickyOffsets();
1327
1777
  startCountdown();
1328
1778
  setInterval(checkStatus, 30000);
1329
-
1330
-