@vheins/local-memory-mcp 0.4.12 → 0.5.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 (201) hide show
  1. package/README.md +21 -0
  2. package/bin/mcp-memory-server.js +1 -1
  3. package/dist/dashboard/public/assets/index-KxrQ6fPK.css +1 -0
  4. package/dist/dashboard/public/assets/index-Lu37nzOi.js +72 -0
  5. package/dist/dashboard/public/index.html +11 -1772
  6. package/dist/dashboard/server.js +115 -5
  7. package/dist/dashboard/server.js.map +1 -1
  8. package/dist/mcp/capabilities.d.ts +22 -0
  9. package/dist/mcp/capabilities.d.ts.map +1 -0
  10. package/dist/mcp/capabilities.js +38 -0
  11. package/dist/mcp/capabilities.js.map +1 -0
  12. package/dist/mcp/client.js +3 -3
  13. package/dist/mcp/client.js.map +1 -1
  14. package/dist/mcp/completion.d.ts +25 -0
  15. package/dist/mcp/completion.d.ts.map +1 -0
  16. package/dist/mcp/completion.js +127 -0
  17. package/dist/mcp/completion.js.map +1 -0
  18. package/dist/mcp/prompts/registry.d.ts +314 -0
  19. package/dist/mcp/prompts/registry.d.ts.map +1 -0
  20. package/dist/mcp/prompts/registry.js +936 -0
  21. package/dist/mcp/prompts/registry.js.map +1 -0
  22. package/dist/mcp/resources/index.d.ts +68 -0
  23. package/dist/mcp/resources/index.d.ts.map +1 -0
  24. package/dist/mcp/resources/index.js +323 -0
  25. package/dist/mcp/resources/index.js.map +1 -0
  26. package/dist/mcp/router.d.ts +14 -0
  27. package/dist/mcp/router.d.ts.map +1 -0
  28. package/dist/mcp/router.js +242 -0
  29. package/dist/mcp/router.js.map +1 -0
  30. package/dist/mcp/server.d.ts +3 -0
  31. package/dist/mcp/server.d.ts.map +1 -0
  32. package/dist/mcp/server.js +338 -0
  33. package/dist/mcp/server.js.map +1 -0
  34. package/dist/mcp/storage/sqlite.d.ts +83 -0
  35. package/dist/mcp/storage/sqlite.d.ts.map +1 -0
  36. package/dist/mcp/storage/sqlite.js +1237 -0
  37. package/dist/mcp/storage/sqlite.js.map +1 -0
  38. package/dist/mcp/storage/vectors.d.ts +19 -0
  39. package/dist/mcp/storage/vectors.d.ts.map +1 -0
  40. package/dist/mcp/storage/vectors.js +74 -0
  41. package/dist/mcp/storage/vectors.js.map +1 -0
  42. package/dist/mcp/storage/vectors.stub.d.ts +12 -0
  43. package/dist/mcp/storage/vectors.stub.d.ts.map +1 -0
  44. package/dist/mcp/storage/vectors.stub.js +88 -0
  45. package/dist/mcp/storage/vectors.stub.js.map +1 -0
  46. package/dist/mcp/tests/client.test.d.ts +2 -0
  47. package/dist/mcp/tests/client.test.d.ts.map +1 -0
  48. package/dist/mcp/tests/client.test.js +130 -0
  49. package/dist/mcp/tests/client.test.js.map +1 -0
  50. package/dist/mcp/tests/dashboard.test.d.ts +2 -0
  51. package/dist/mcp/tests/dashboard.test.d.ts.map +1 -0
  52. package/dist/mcp/tests/dashboard.test.js +370 -0
  53. package/dist/mcp/tests/dashboard.test.js.map +1 -0
  54. package/dist/mcp/tests/e2e.test.d.ts +2 -0
  55. package/dist/mcp/tests/e2e.test.d.ts.map +1 -0
  56. package/dist/mcp/tests/e2e.test.js +250 -0
  57. package/dist/mcp/tests/e2e.test.js.map +1 -0
  58. package/dist/mcp/tests/index.test.d.ts +2 -0
  59. package/dist/mcp/tests/index.test.d.ts.map +1 -0
  60. package/dist/mcp/tests/index.test.js +185 -0
  61. package/dist/mcp/tests/index.test.js.map +1 -0
  62. package/dist/mcp/tests/logger.test.d.ts +2 -0
  63. package/dist/mcp/tests/logger.test.d.ts.map +1 -0
  64. package/dist/mcp/tests/logger.test.js +104 -0
  65. package/dist/mcp/tests/logger.test.js.map +1 -0
  66. package/dist/mcp/tests/memory.bulk.test.d.ts +2 -0
  67. package/dist/mcp/tests/memory.bulk.test.d.ts.map +1 -0
  68. package/dist/mcp/tests/memory.bulk.test.js +52 -0
  69. package/dist/mcp/tests/memory.bulk.test.js.map +1 -0
  70. package/dist/mcp/tests/memory.search.test.d.ts +2 -0
  71. package/dist/mcp/tests/memory.search.test.d.ts.map +1 -0
  72. package/dist/mcp/tests/memory.search.test.js +181 -0
  73. package/dist/mcp/tests/memory.search.test.js.map +1 -0
  74. package/dist/mcp/tests/normalize.test.d.ts +2 -0
  75. package/dist/mcp/tests/normalize.test.d.ts.map +1 -0
  76. package/dist/mcp/tests/normalize.test.js +181 -0
  77. package/dist/mcp/tests/normalize.test.js.map +1 -0
  78. package/dist/mcp/tests/query-expander.test.d.ts +2 -0
  79. package/dist/mcp/tests/query-expander.test.d.ts.map +1 -0
  80. package/dist/mcp/tests/query-expander.test.js +33 -0
  81. package/dist/mcp/tests/query-expander.test.js.map +1 -0
  82. package/dist/mcp/tests/router.test.d.ts +2 -0
  83. package/dist/mcp/tests/router.test.d.ts.map +1 -0
  84. package/dist/mcp/tests/router.test.js +470 -0
  85. package/dist/mcp/tests/router.test.js.map +1 -0
  86. package/dist/mcp/tests/sqlite.test.d.ts +2 -0
  87. package/dist/mcp/tests/sqlite.test.d.ts.map +1 -0
  88. package/dist/mcp/tests/sqlite.test.js +367 -0
  89. package/dist/mcp/tests/sqlite.test.js.map +1 -0
  90. package/dist/mcp/tests/tasks-search.test.d.ts +2 -0
  91. package/dist/mcp/tests/tasks-search.test.d.ts.map +1 -0
  92. package/dist/mcp/tests/tasks-search.test.js +154 -0
  93. package/dist/mcp/tests/tasks-search.test.js.map +1 -0
  94. package/dist/mcp/tests/tasks-transition.test.d.ts +2 -0
  95. package/dist/mcp/tests/tasks-transition.test.d.ts.map +1 -0
  96. package/dist/mcp/tests/tasks-transition.test.js +174 -0
  97. package/dist/mcp/tests/tasks-transition.test.js.map +1 -0
  98. package/dist/mcp/tests/tasks.bulk.test.d.ts +2 -0
  99. package/dist/mcp/tests/tasks.bulk.test.d.ts.map +1 -0
  100. package/dist/mcp/tests/tasks.bulk.test.js +254 -0
  101. package/dist/mcp/tests/tasks.bulk.test.js.map +1 -0
  102. package/dist/mcp/tests/tasks.e2e.test.d.ts +2 -0
  103. package/dist/mcp/tests/tasks.e2e.test.d.ts.map +1 -0
  104. package/dist/mcp/tests/tasks.e2e.test.js +289 -0
  105. package/dist/mcp/tests/tasks.e2e.test.js.map +1 -0
  106. package/dist/mcp/tests/tasks.pending-limit-refined.test.d.ts +2 -0
  107. package/dist/mcp/tests/tasks.pending-limit-refined.test.d.ts.map +1 -0
  108. package/dist/mcp/tests/tasks.pending-limit-refined.test.js +72 -0
  109. package/dist/mcp/tests/tasks.pending-limit-refined.test.js.map +1 -0
  110. package/dist/mcp/tests/v2-features.test.d.ts +2 -0
  111. package/dist/mcp/tests/v2-features.test.d.ts.map +1 -0
  112. package/dist/mcp/tests/v2-features.test.js +209 -0
  113. package/dist/mcp/tests/v2-features.test.js.map +1 -0
  114. package/dist/mcp/tools/memory.acknowledge.d.ts +4 -0
  115. package/dist/mcp/tools/memory.acknowledge.d.ts.map +1 -0
  116. package/dist/mcp/tools/memory.acknowledge.js +30 -0
  117. package/dist/mcp/tools/memory.acknowledge.js.map +1 -0
  118. package/dist/mcp/tools/memory.bulk-delete.d.ts +4 -0
  119. package/dist/mcp/tools/memory.bulk-delete.d.ts.map +1 -0
  120. package/dist/mcp/tools/memory.bulk-delete.js +39 -0
  121. package/dist/mcp/tools/memory.bulk-delete.js.map +1 -0
  122. package/dist/mcp/tools/memory.delete.d.ts +9 -0
  123. package/dist/mcp/tools/memory.delete.d.ts.map +1 -0
  124. package/dist/mcp/tools/memory.delete.js +39 -0
  125. package/dist/mcp/tools/memory.delete.js.map +1 -0
  126. package/dist/mcp/tools/memory.recap.d.ts +4 -0
  127. package/dist/mcp/tools/memory.recap.d.ts.map +1 -0
  128. package/dist/mcp/tools/memory.recap.js +90 -0
  129. package/dist/mcp/tools/memory.recap.js.map +1 -0
  130. package/dist/mcp/tools/memory.search.d.ts +5 -0
  131. package/dist/mcp/tools/memory.search.d.ts.map +1 -0
  132. package/dist/mcp/tools/memory.search.js +134 -0
  133. package/dist/mcp/tools/memory.search.js.map +1 -0
  134. package/dist/mcp/tools/memory.store.d.ts +5 -0
  135. package/dist/mcp/tools/memory.store.d.ts.map +1 -0
  136. package/dist/mcp/tools/memory.store.js +117 -0
  137. package/dist/mcp/tools/memory.store.js.map +1 -0
  138. package/dist/mcp/tools/memory.summarize.d.ts +4 -0
  139. package/dist/mcp/tools/memory.summarize.d.ts.map +1 -0
  140. package/dist/mcp/tools/memory.summarize.js +31 -0
  141. package/dist/mcp/tools/memory.summarize.js.map +1 -0
  142. package/dist/mcp/tools/memory.synthesize.d.ts +14 -0
  143. package/dist/mcp/tools/memory.synthesize.d.ts.map +1 -0
  144. package/dist/mcp/tools/memory.synthesize.js +228 -0
  145. package/dist/mcp/tools/memory.synthesize.js.map +1 -0
  146. package/dist/mcp/tools/memory.update.d.ts +5 -0
  147. package/dist/mcp/tools/memory.update.d.ts.map +1 -0
  148. package/dist/mcp/tools/memory.update.js +73 -0
  149. package/dist/mcp/tools/memory.update.js.map +1 -0
  150. package/dist/mcp/tools/schemas.d.ts +2762 -0
  151. package/dist/mcp/tools/schemas.d.ts.map +1 -0
  152. package/dist/mcp/tools/schemas.js +952 -0
  153. package/dist/mcp/tools/schemas.js.map +1 -0
  154. package/dist/mcp/tools/task.bulk-manage.d.ts +4 -0
  155. package/dist/mcp/tools/task.bulk-manage.d.ts.map +1 -0
  156. package/dist/mcp/tools/task.bulk-manage.js +146 -0
  157. package/dist/mcp/tools/task.bulk-manage.js.map +1 -0
  158. package/dist/mcp/tools/task.manage.d.ts +16 -0
  159. package/dist/mcp/tools/task.manage.d.ts.map +1 -0
  160. package/dist/mcp/tools/task.manage.js +395 -0
  161. package/dist/mcp/tools/task.manage.js.map +1 -0
  162. package/dist/mcp/types.d.ts +87 -0
  163. package/dist/mcp/types.d.ts.map +1 -0
  164. package/dist/mcp/types.js +3 -0
  165. package/dist/mcp/types.js.map +1 -0
  166. package/dist/mcp/utils/completion.d.ts +2 -0
  167. package/dist/mcp/utils/completion.d.ts.map +1 -0
  168. package/dist/mcp/utils/completion.js +28 -0
  169. package/dist/mcp/utils/completion.js.map +1 -0
  170. package/dist/mcp/utils/git-scope.d.ts +8 -0
  171. package/dist/mcp/utils/git-scope.d.ts.map +1 -0
  172. package/dist/mcp/utils/git-scope.js +38 -0
  173. package/dist/mcp/utils/git-scope.js.map +1 -0
  174. package/dist/mcp/utils/logger.d.ts +25 -0
  175. package/dist/mcp/utils/logger.d.ts.map +1 -0
  176. package/dist/mcp/utils/logger.js +152 -0
  177. package/dist/mcp/utils/logger.js.map +1 -0
  178. package/dist/mcp/utils/mcp-response.d.ts +96 -0
  179. package/dist/mcp/utils/mcp-response.d.ts.map +1 -0
  180. package/dist/mcp/utils/mcp-response.js +131 -0
  181. package/dist/mcp/utils/mcp-response.js.map +1 -0
  182. package/dist/mcp/utils/normalize.d.ts +9 -0
  183. package/dist/mcp/utils/normalize.d.ts.map +1 -0
  184. package/dist/mcp/utils/normalize.js +62 -0
  185. package/dist/mcp/utils/normalize.js.map +1 -0
  186. package/dist/mcp/utils/pagination.d.ts +6 -0
  187. package/dist/mcp/utils/pagination.d.ts.map +1 -0
  188. package/dist/mcp/utils/pagination.js +32 -0
  189. package/dist/mcp/utils/pagination.js.map +1 -0
  190. package/dist/mcp/utils/query-expander.d.ts +2 -0
  191. package/dist/mcp/utils/query-expander.d.ts.map +1 -0
  192. package/dist/mcp/utils/query-expander.js +29 -0
  193. package/dist/mcp/utils/query-expander.js.map +1 -0
  194. package/dist/storage/sqlite.d.ts +3 -0
  195. package/dist/storage/sqlite.d.ts.map +1 -1
  196. package/dist/storage/sqlite.js +38 -9
  197. package/dist/storage/sqlite.js.map +1 -1
  198. package/package.json +17 -5
  199. package/dist/dashboard/public/app.js +0 -2779
  200. package/dist/dashboard/public/chart.js +0 -0
  201. package/dist/dashboard/public/screenshot.png +0 -0
@@ -1,2779 +0,0 @@
1
- let currentRepo = null;
2
- let currentMemories = [];
3
- let currentPage = 1;
4
- let pageSize = 10;
5
- let totalPages = 1;
6
- let totalItems = 0;
7
- let selectedIds = new Set();
8
- let currentPaginatedData = [];
9
- let taskPagination = {
10
- backlog: { page: 1, pageSize: 20, hasMore: true, loading: false },
11
- todo: { page: 1, pageSize: 20, hasMore: true, loading: false },
12
- in_progress: { page: 1, pageSize: 20, hasMore: true, loading: false },
13
- completed: { page: 1, pageSize: 20, hasMore: true, loading: false }
14
- };
15
- let charts = {};
16
- let lastSyncTime = Date.now();
17
- let countdownSeconds = 30;
18
- let countdownInterval = null;
19
- let recentActions = [];
20
- let recentActionsPage = 1;
21
- let recentActionsPageSize = 5;
22
- let recentActionsTotalPages = 1;
23
- let recentActionsTotalItems = 0;
24
- let activeEditMemoryId = null;
25
- let currentDrawerMemoryId = null;
26
- let availableRepos = [];
27
- let isRepoSidebarCollapsed = false;
28
- let pinnedRepoOrder = [];
29
- let draggedPinnedRepo = null;
30
- let isGlobalFilterActive = false;
31
-
32
- function syncStickyOffsets() {
33
- const topBar = document.getElementById('mainTopBar');
34
- const tabNav = document.querySelector('.sticky-tab-nav');
35
- if (!topBar) return;
36
-
37
- const topBarHeight = Math.ceil(topBar.getBoundingClientRect().height);
38
- const tabNavHeight = tabNav ? Math.ceil(tabNav.getBoundingClientRect().height) : 0;
39
-
40
- document.documentElement.style.setProperty('--dashboard-header-offset', `${topBarHeight}px`);
41
- document.documentElement.style.setProperty('--dashboard-tab-offset', `${tabNavHeight}px`);
42
- }
43
-
44
- async function loadRecentActions(page = recentActionsPage) {
45
- try {
46
- let url = `/api/recent-actions?page=${page}&pageSize=${recentActionsPageSize}`;
47
- if (currentRepo) url += `&repo=${encodeURIComponent(currentRepo)}`;
48
- const response = await fetch(url);
49
- const data = await response.json();
50
- recentActions = data.actions || [];
51
- recentActionsPage = data.pagination?.page ?? page;
52
- recentActionsTotalPages = data.pagination?.totalPages ?? 1;
53
- recentActionsTotalItems = data.pagination?.totalItems ?? recentActions.length;
54
- renderRecentActions();
55
- } catch (err) {
56
- console.error('Failed to load recent actions:', err);
57
- }
58
- }
59
-
60
- function goToRecentActionsPage(page) {
61
- if (page < 1 || page > recentActionsTotalPages) return;
62
- recentActionsPage = page;
63
- loadRecentActions(page);
64
- }
65
-
66
- function formatActionDate(dateStr) {
67
- const date = new Date(dateStr);
68
- const now = new Date();
69
- const diff = Math.floor((now - date) / 1000);
70
- if (diff < 60) return `${diff}s ago`;
71
- if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
72
- if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
73
- return date.toLocaleDateString();
74
- }
75
-
76
- function getActionIcon(action) {
77
- const icons = {
78
- search: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>',
79
- read: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>',
80
- write: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>',
81
- update: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>',
82
- delete: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>'
83
- };
84
- return icons[action] || icons.search;
85
- }
86
-
87
- function getActionColor(action) {
88
- const colors = {
89
- search: 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400',
90
- read: 'bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-400',
91
- write: 'bg-purple-100 dark:bg-purple-900 text-purple-600 dark:text-purple-400',
92
- update: 'bg-yellow-100 dark:bg-yellow-900 text-yellow-600 dark:text-yellow-400',
93
- delete: 'bg-red-100 dark:bg-red-900 text-red-600 dark:text-red-400'
94
- };
95
- return colors[action] || colors.search;
96
- }
97
-
98
- function getActionBubbleColor(action) {
99
- const colors = {
100
- search: 'from-blue-500 to-blue-600',
101
- read: 'from-emerald-500 to-emerald-600',
102
- write: 'from-indigo-500 to-indigo-600',
103
- update: 'from-amber-500 to-amber-600',
104
- delete: 'from-rose-500 to-rose-600'
105
- };
106
- return colors[action] || 'from-slate-500 to-slate-600';
107
- }
108
-
109
- function renderActionBubble(action) {
110
- // Agent Request Bubble
111
- let mainText = '';
112
- let subText = '';
113
-
114
- if (action.action === 'search') {
115
- mainText = `🔍 "${action.query || ''}"`;
116
- subText = action.result_count != null ? `${action.result_count} result${action.result_count !== 1 ? 's' : ''} found` : '';
117
- } else if (action.task_id) {
118
- mainText = action.task_title || action.task_code || action.task_id.substring(0, 8);
119
- const verb = { write: '💾 Created Task', update: '🔄 Updated Task', delete: '🗑️ Deleted Task' }[action.action] || action.action;
120
- subText = action.task_code ? `${verb} [${action.task_code}]` : verb;
121
- } else {
122
- mainText = action.memory_title || (action.memory_id ? action.memory_id.substring(0, 8) + '…' : '—');
123
- const typeLabel = action.memory_type ? `[${action.memory_type}]` : '';
124
- const verbs = {
125
- write: '💾 Stored',
126
- update: '🔄 Updated',
127
- delete: '🗑️ Deleted',
128
- read: '📖 Read',
129
- agent_handoff: '🤝 Handoff',
130
- agent_registered: '📝 Registration'
131
- };
132
- const verb = verbs[action.action] || verbs[action.memory_type] || action.action;
133
- subText = [verb, typeLabel].filter(Boolean).join(' ');
134
- }
135
-
136
- const colorGradient = getActionBubbleColor(action.action);
137
- const agentBubble = `
138
- <div class="flex flex-col agent-align">
139
- <div class="chat-bubble chat-bubble-agent bg-gradient-to-br ${colorGradient}">
140
- <div class="flex items-center gap-1.5 mb-1 opacity-80">
141
- <svg class="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">${getActionIcon(action.action)}</svg>
142
- <span class="text-[10px] font-bold uppercase tracking-wider">${action.action}</span>
143
- </div>
144
- <p class="text-sm font-medium leading-snug break-words">${mainText}</p>
145
- ${subText ? `<p class="text-[10px] mt-1 opacity-70 font-semibold uppercase tracking-tight">${subText}</p>` : ''}
146
- </div>
147
- <span class="chat-timestamp">${formatActionDate(action.created_at)}</span>
148
- </div>
149
- `;
150
-
151
- // MCP Response Bubble (if exists)
152
- let mcpBubble = '';
153
- if (action.response) {
154
- let responseContent = '';
155
- try {
156
- const resp = typeof action.response === 'string' ? JSON.parse(action.response) : action.response;
157
- if (resp.content && Array.isArray(resp.content)) {
158
- responseContent = resp.content
159
- .filter(c => c.type === 'text')
160
- .map(c => c.text)
161
- .join('\n');
162
- } else if (resp.message) {
163
- responseContent = resp.message;
164
- } else {
165
- responseContent = JSON.stringify(resp, null, 2);
166
- }
167
- } catch (e) {
168
- responseContent = action.response;
169
- }
170
-
171
- mcpBubble = `
172
- <div class="flex flex-col mcp-align">
173
- <div class="chat-bubble chat-bubble-mcp">
174
- <div class="flex items-center gap-1.5 mb-1 opacity-60">
175
- <div class="w-2 h-2 rounded-full bg-sky-500"></div>
176
- <span class="text-[10px] font-bold uppercase tracking-wider">MCP REPLY</span>
177
- </div>
178
- <div class="markdown-body text-xs prose-sm prose-slate dark:prose-invert">
179
- ${renderMarkdown(responseContent)}
180
- </div>
181
- </div>
182
- </div>
183
- `;
184
- }
185
-
186
- return agentBubble + mcpBubble;
187
- }
188
-
189
- function renderRecentActions() {
190
- const container = document.getElementById('recentQueries');
191
- const paginationEl = document.getElementById('recentActionsPagination');
192
-
193
- if (recentActions.length === 0 && recentActionsPage === 1) {
194
- container.innerHTML = '<div class="text-gray-400 text-sm text-center py-6">No recent actions</div>';
195
- if (paginationEl) paginationEl.innerHTML = '';
196
- return;
197
- }
198
-
199
- container.innerHTML = recentActions.map(renderActionBubble).join('');
200
-
201
- if (paginationEl) {
202
- if (recentActionsTotalPages <= 1) {
203
- paginationEl.innerHTML = '';
204
- } else {
205
- const prevDisabled = recentActionsPage <= 1;
206
- const nextDisabled = recentActionsPage >= recentActionsTotalPages;
207
- paginationEl.innerHTML = `
208
- <div class="flex items-center justify-between pt-2 border-t border-gray-100 dark:border-gray-700">
209
- <span class="text-xs text-gray-500">${recentActionsPage} / ${recentActionsTotalPages}</span>
210
- <div class="flex gap-1">
211
- <button onclick="goToRecentActionsPage(${recentActionsPage - 1})"
212
- class="px-2 py-1 rounded text-xs font-medium ${prevDisabled ? 'text-gray-300 dark:text-gray-600 cursor-not-allowed' : 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
213
- ${prevDisabled ? 'disabled' : ''}>&#8592;</button>
214
- <button onclick="goToRecentActionsPage(${recentActionsPage + 1})"
215
- class="px-2 py-1 rounded text-xs font-medium ${nextDisabled ? 'text-gray-300 dark:text-gray-600 cursor-not-allowed' : 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
216
- ${nextDisabled ? 'disabled' : ''}>&#8594;</button>
217
- </div>
218
- </div>
219
- `;
220
- }
221
- }
222
- }
223
-
224
- function searchFromRecent(query) {
225
- document.getElementById('searchInput').value = query;
226
- currentPage = 1;
227
- loadMemories();
228
- }
229
-
230
- function getRepoInitials(repo) {
231
- return repo
232
- .split(/[\/\-_.]/)
233
- .filter(Boolean)
234
- .slice(0, 2)
235
- .map((part) => part[0]?.toUpperCase() || '')
236
- .join('')
237
- .slice(0, 2) || 'RP';
238
- }
239
-
240
- function getRepoLastUpdatedLabel(repoMeta) {
241
- if (!repoMeta?.last_updated_at) return 'No updates yet';
242
- return `Updated ${formatDate(repoMeta.last_updated_at)}`;
243
- }
244
-
245
- function applyRepoSidebarState() {
246
- const layout = document.getElementById('appLayout');
247
- const icon = document.getElementById('repoSidebarCollapseIcon');
248
- const button = document.getElementById('repoSidebarCollapseToggle');
249
- if (!layout || !icon || !button) return;
250
-
251
- layout.classList.toggle('repo-sidebar-collapsed', isRepoSidebarCollapsed);
252
- icon.style.transform = isRepoSidebarCollapsed ? 'rotate(180deg)' : 'rotate(0deg)';
253
- button.title = isRepoSidebarCollapsed ? 'Expand repositories' : 'Collapse repositories';
254
- button.setAttribute('aria-label', button.title);
255
- }
256
-
257
- function toggleRepoSidebarCollapse() {
258
- isRepoSidebarCollapsed = !isRepoSidebarCollapsed;
259
- localStorage.setItem('repoSidebarCollapsed', isRepoSidebarCollapsed ? '1' : '0');
260
- applyRepoSidebarState();
261
- renderRepoSidebar();
262
- }
263
-
264
- function updateCollapsedRepoSummary() {
265
- const initials = document.getElementById('repoCollapsedSummaryInitials');
266
- const count = document.getElementById('repoCollapsedSummaryCount');
267
- const button = document.getElementById('repoCollapsedSummaryButton');
268
- if (!initials || !count || !button) return;
269
-
270
- const activeRepo = availableRepos.find((item) => item.repo === currentRepo);
271
- initials.textContent = getRepoInitials(currentRepo || 'RP');
272
- count.textContent = String(activeRepo?.memory_count ?? 0);
273
- button.title = activeRepo
274
- ? `${activeRepo.repo} • ${activeRepo.memory_count} memories`
275
- : 'Active repository';
276
- }
277
-
278
- function persistPinnedRepos() {
279
- localStorage.setItem('pinnedRepos', JSON.stringify(pinnedRepoOrder));
280
- }
281
-
282
- function initPinnedRepos() {
283
- try {
284
- const raw = localStorage.getItem('pinnedRepos');
285
- const parsed = raw ? JSON.parse(raw) : [];
286
- pinnedRepoOrder = Array.isArray(parsed) ? parsed : [];
287
- } catch {
288
- pinnedRepoOrder = [];
289
- }
290
- }
291
-
292
- function isRepoPinned(repo) {
293
- return pinnedRepoOrder.includes(repo);
294
- }
295
-
296
- function getOrderedPinnedRepos(repos) {
297
- return repos
298
- .filter((item) => isRepoPinned(item.repo))
299
- .sort((a, b) => pinnedRepoOrder.indexOf(a.repo) - pinnedRepoOrder.indexOf(b.repo));
300
- }
301
-
302
- function togglePinnedRepo(repo, event) {
303
- if (event) {
304
- event.preventDefault();
305
- event.stopPropagation();
306
- }
307
-
308
- if (!repo) return;
309
-
310
- if (isRepoPinned(repo)) pinnedRepoOrder = pinnedRepoOrder.filter((item) => item !== repo);
311
- else pinnedRepoOrder.push(repo);
312
-
313
- persistPinnedRepos();
314
- renderRepoSidebar();
315
- }
316
-
317
- function startPinnedRepoDrag(repo, event) {
318
- if (!isRepoPinned(repo)) return;
319
- draggedPinnedRepo = repo;
320
- event.dataTransfer.effectAllowed = 'move';
321
- event.dataTransfer.setData('text/plain', repo);
322
- event.currentTarget.classList.add('dragging');
323
- }
324
-
325
- function overPinnedRepoDrag(repo, event) {
326
- if (!draggedPinnedRepo || draggedPinnedRepo === repo || !isRepoPinned(repo)) return;
327
- event.preventDefault();
328
- event.dataTransfer.dropEffect = 'move';
329
- event.currentTarget.classList.add('drag-target');
330
- }
331
-
332
- function leavePinnedRepoDrag(event) {
333
- event.currentTarget.classList.remove('drag-target');
334
- }
335
-
336
- function dropPinnedRepo(repo, event) {
337
- event.preventDefault();
338
- event.currentTarget.classList.remove('drag-target');
339
-
340
- const draggedRepo = draggedPinnedRepo || event.dataTransfer.getData('text/plain');
341
- if (!draggedRepo || draggedRepo === repo || !isRepoPinned(draggedRepo) || !isRepoPinned(repo)) return;
342
-
343
- const nextOrder = pinnedRepoOrder.filter((item) => item !== draggedRepo);
344
- const targetIndex = nextOrder.indexOf(repo);
345
- nextOrder.splice(targetIndex, 0, draggedRepo);
346
- pinnedRepoOrder = nextOrder;
347
- draggedPinnedRepo = null;
348
- persistPinnedRepos();
349
- renderRepoSidebar();
350
- }
351
-
352
- function endPinnedRepoDrag(event) {
353
- draggedPinnedRepo = null;
354
- event.currentTarget.classList.remove('dragging');
355
- document.querySelectorAll('.repo-item.drag-target').forEach((el) => el.classList.remove('drag-target'));
356
- }
357
-
358
- function renderRepoSidebar() {
359
- const desktopQuery = document.getElementById('repoSearchInput')?.value?.trim().toLowerCase() || '';
360
- const mobileQuery = document.getElementById('repoSearchInputMobile')?.value?.trim().toLowerCase() || '';
361
- const query = desktopQuery || mobileQuery;
362
- const repos = availableRepos.filter((item) => item.repo.toLowerCase().includes(query));
363
- const pinnedItems = getOrderedPinnedRepos(repos);
364
- const unpinnedItems = repos.filter((item) => !isRepoPinned(item.repo));
365
-
366
- const renderItems = (items) => items.map((item) => `
367
- <div class="repo-item ${item.repo === currentRepo ? 'active' : ''} ${isRepoPinned(item.repo) ? 'pinned-item' : ''}" onclick="setCurrentRepo('${item.repo.replace(/'/g, "\\'")}')" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault(); setCurrentRepo('${item.repo.replace(/'/g, "\\'")}')}" title="${item.repo} • ${item.memory_count} memories" role="button" tabindex="0" ${isRepoPinned(item.repo) ? `draggable="true" ondragstart="startPinnedRepoDrag('${item.repo.replace(/'/g, "\\'")}', event)" ondragover="overPinnedRepoDrag('${item.repo.replace(/'/g, "\\'")}', event)" ondragleave="leavePinnedRepoDrag(event)" ondrop="dropPinnedRepo('${item.repo.replace(/'/g, "\\'")}', event)" ondragend="endPinnedRepoDrag(event)"` : ''}>
368
- <span class="repo-avatar">${getRepoInitials(item.repo)}${isRepoPinned(item.repo) ? '<span class="repo-pinned-mark">★</span>' : ''}</span>
369
- <span class="repo-item-copy min-w-0 flex-1">
370
- <span class="flex items-center justify-between gap-3">
371
- <span class="flex items-center gap-2 min-w-0">
372
- ${isRepoPinned(item.repo) ? `
373
- <span class="repo-drag-handle" title="Drag to reorder pinned repositories" aria-hidden="true">
374
- <svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
375
- <path d="M7 4a1.5 1.5 0 110 3 1.5 1.5 0 010-3Zm6 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3ZM7 8.5a1.5 1.5 0 110 3 1.5 1.5 0 010-3Zm6 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3ZM7 13a1.5 1.5 0 110 3 1.5 1.5 0 010-3Zm6 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3Z"/>
376
- </svg>
377
- </span>
378
- ` : ''}
379
- <span class="block truncate text-sm font-semibold text-gray-900 dark:text-gray-100">${item.repo}</span>
380
- </span>
381
- <span class="text-[11px] font-semibold text-sky-700 dark:text-sky-200">${item.memory_count}</span>
382
- </span>
383
- <span class="block truncate text-xs text-gray-500 dark:text-gray-400">${item.repo === currentRepo ? 'Active repository' : getRepoLastUpdatedLabel(item)}</span>
384
- </span>
385
- <button class="repo-item-pin ${isRepoPinned(item.repo) ? 'pinned' : ''}" onclick="togglePinnedRepo('${item.repo.replace(/'/g, "\\'")}', event)" title="${isRepoPinned(item.repo) ? 'Unpin repository' : 'Pin repository'}" aria-label="${isRepoPinned(item.repo) ? 'Unpin repository' : 'Pin repository'}">
386
- <svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
387
- <path d="M12.586 2.586a2 2 0 0 1 2.828 0l2 2a2 2 0 0 1 0 2.828l-1.793 1.793-.914 4.57a1 1 0 0 1-.271.51l-1.414 1.414a1 1 0 0 1-1.414 0l-2.122-2.121-4.172 4.171a1 1 0 1 1-1.414-1.414l4.171-4.172-2.12-2.12a1 1 0 0 1 0-1.415l1.413-1.414a1 1 0 0 1 .51-.27l4.57-.915 1.792-1.793Z"/>
388
- </svg>
389
- </button>
390
- ${item.repo === currentRepo ? '<span class="repo-active-dot"></span>' : ''}
391
- </div>
392
- `).join('');
393
-
394
- const renderGroups = () => {
395
- const groups = [];
396
- if (pinnedItems.length > 0) {
397
- groups.push(`
398
- <section class="repo-group">
399
- <div class="repo-group-label">Pinned</div>
400
- <div class="space-y-2">${renderItems(pinnedItems)}</div>
401
- </section>
402
- `);
403
- }
404
- if (unpinnedItems.length > 0) {
405
- groups.push(`
406
- <section class="repo-group">
407
- <div class="repo-group-label">${pinnedItems.length > 0 ? 'All Repositories' : 'Repositories'}</div>
408
- <div class="space-y-2">${renderItems(unpinnedItems)}</div>
409
- </section>
410
- `);
411
- }
412
- return groups.join('');
413
- };
414
-
415
- document.getElementById('repoCountBadge').textContent = String(availableRepos.length);
416
- document.getElementById('currentRepoLabel').textContent = currentRepo || 'No repository';
417
- updateCollapsedRepoSummary();
418
-
419
- if (repos.length === 0) {
420
- document.getElementById('repoSidebarList').innerHTML = '<div class="text-sm text-gray-500 dark:text-gray-400 px-3 py-4">No repositories found.</div>';
421
- const mobile = document.getElementById('repoSidebarListMobile');
422
- if (mobile) mobile.innerHTML = '<div class="text-sm text-gray-500 dark:text-gray-400 px-3 py-4">No repositories found.</div>';
423
- return;
424
- }
425
-
426
- document.getElementById('repoSidebarList').innerHTML = renderGroups();
427
- const mobile = document.getElementById('repoSidebarListMobile');
428
- if (mobile) mobile.innerHTML = renderGroups();
429
- syncStickyOffsets();
430
- }
431
-
432
- async function setCurrentRepo(repo) {
433
- if (!repo || repo === currentRepo) return;
434
- currentRepo = repo;
435
- currentPage = 1;
436
- recentActionsPage = 1;
437
- selectedIds.clear();
438
-
439
- // Clear search input on repo change
440
- const taskSearch = document.getElementById('taskSearchInput');
441
- if (taskSearch) taskSearch.value = '';
442
-
443
- localStorage.setItem('selectedRepo', currentRepo);
444
- renderRepoSidebar();
445
- closeRepoSidebarDrawer();
446
- await Promise.all([
447
- loadStats(),
448
- loadMemories(),
449
- loadRecentActions(),
450
- loadTasks()
451
- ]);
452
- syncStickyOffsets();
453
- }
454
-
455
- function openRepoSidebarDrawer() {
456
- document.getElementById('repoSidebarDrawer').classList.remove('hidden');
457
- document.body.classList.add('drawer-open');
458
- }
459
-
460
- function closeRepoSidebarDrawer() {
461
- document.getElementById('repoSidebarDrawer').classList.add('hidden');
462
- if (!document.getElementById('memoryDrawer') || document.getElementById('memoryDrawer').classList.contains('hidden')) {
463
- document.body.classList.remove('drawer-open');
464
- }
465
- }
466
-
467
- function initTheme() {
468
- const saved = localStorage.getItem('theme') || 'light';
469
- document.documentElement.classList.toggle('dark', saved === 'dark');
470
- }
471
-
472
- function initRepoSidebarState() {
473
- isRepoSidebarCollapsed = localStorage.getItem('repoSidebarCollapsed') === '1';
474
- applyRepoSidebarState();
475
- }
476
-
477
- function startCountdown() {
478
- if (countdownInterval) clearInterval(countdownInterval);
479
- countdownSeconds = 30;
480
- updateCountdown();
481
-
482
- countdownInterval = setInterval(() => {
483
- countdownSeconds--;
484
- if (countdownSeconds <= 0) {
485
- countdownSeconds = 30;
486
- loadData();
487
- }
488
- updateCountdown();
489
- }, 1000);
490
- }
491
-
492
- function updateCountdown() {
493
- const fill = document.getElementById('countdownFill');
494
- const status = document.getElementById('syncStatus');
495
- const percent = (countdownSeconds / 30) * 100;
496
- fill.style.width = percent + '%';
497
-
498
- if (countdownSeconds <= 5) {
499
- fill.style.background = '#ef4444';
500
- } else if (countdownSeconds <= 10) {
501
- fill.style.background = '#f97316';
502
- } else {
503
- fill.style.background = '#3b82f6';
504
- }
505
-
506
- status.textContent = `Synced ${countdownSeconds}s ago`;
507
- }
508
-
509
- ['themeToggle', 'themeToggleMobile'].forEach(id => {
510
- const btn = document.getElementById(id);
511
- if (btn) {
512
- btn.addEventListener('click', () => {
513
- const isDark = document.documentElement.classList.toggle('dark');
514
- localStorage.setItem('theme', isDark ? 'dark' : 'light');
515
- scheduleTabIndicatorPosition(currentTab);
516
- });
517
- }
518
- });
519
-
520
- document.addEventListener('keydown', (e) => {
521
- if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;
522
-
523
- if (e.key === 'Escape') closeDrawer();
524
- else if (e.key === 'r' || e.key === 'R') loadData();
525
- else if (e.key === '/') {
526
- e.preventDefault();
527
- document.getElementById('searchInput')?.focus();
528
- }
529
- });
530
-
531
- async function checkStatus() {
532
- try {
533
- const response = await fetch('/api/health');
534
- const data = await response.json();
535
- const dot = document.getElementById('statusDot');
536
- const text = document.getElementById('statusText');
537
-
538
- if (data.connected) {
539
- dot.classList.remove('bg-gray-400');
540
- dot.classList.add('bg-green-500');
541
- text.textContent = 'Connected';
542
- } else {
543
- dot.classList.remove('bg-green-500');
544
- dot.classList.add('bg-gray-400');
545
- text.textContent = 'Disconnected';
546
- }
547
-
548
- const dbPathLabel = document.getElementById('dbPathLabel');
549
- if (dbPathLabel && data.dbPath) {
550
- dbPathLabel.textContent = data.dbPath.split(/[/\\]/).pop() || data.dbPath;
551
- dbPathLabel.title = data.dbPath;
552
- }
553
-
554
- const appVersion = document.getElementById('appVersion');
555
- if (appVersion && data.version) {
556
- appVersion.textContent = `v${data.version}`;
557
- }
558
-
559
- const summary = document.getElementById('memorySummaryLabel');
560
- if (summary) {
561
- summary.textContent = `${data.memoryCount || 0} memories indexed`;
562
- }
563
- } catch (err) {
564
- console.error('Status check failed:', err);
565
- const dot = document.getElementById('statusDot');
566
- dot.classList.remove('bg-green-500');
567
- dot.classList.add('bg-gray-400');
568
- document.getElementById('statusText').textContent = 'Error';
569
- }
570
- }
571
-
572
- async function loadRepos() {
573
- try {
574
- const response = await fetch('/api/repos');
575
- const data = await response.json();
576
- if (data.repos && data.repos.length > 0) {
577
- availableRepos = data.repos;
578
- const savedRepo = localStorage.getItem('selectedRepo');
579
- const repoNames = availableRepos.map((item) => item.repo);
580
- if (savedRepo && repoNames.includes(savedRepo)) {
581
- currentRepo = savedRepo;
582
- } else if (!currentRepo) {
583
- currentRepo = availableRepos[0].repo;
584
- }
585
- localStorage.setItem('selectedRepo', currentRepo);
586
- renderRepoSidebar();
587
- } else {
588
- availableRepos = [];
589
- currentRepo = null;
590
- renderRepoSidebar();
591
- }
592
- } catch (err) {
593
- console.error('Failed to load repos:', err);
594
- availableRepos = [];
595
- document.getElementById('repoSidebarList').innerHTML = '<div class="text-sm text-red-500 px-3 py-4">Failed to load repositories.</div>';
596
- }
597
- }
598
-
599
- function formatTokens(num) {
600
- if (!num) return '0';
601
- if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
602
- if (num >= 1000) return (num / 1000).toFixed(1) + 'k';
603
- return num.toString();
604
- }
605
-
606
- function formatDuration(seconds) {
607
- if (!seconds || seconds <= 0) return '0m';
608
- if (seconds < 60) return `${Math.round(seconds)}s`;
609
- const minutes = seconds / 60;
610
- if (minutes < 60) return `${Math.round(minutes)}m`;
611
- const hours = minutes / 60;
612
- if (hours < 24) return `${Math.round(hours)}h`;
613
- return `${Math.round(hours / 24)}d`;
614
- }
615
-
616
- async function loadStats() {
617
- try {
618
- const url = currentRepo ? `/api/stats?repo=${encodeURIComponent(currentRepo)}` : '/api/stats';
619
- const response = await fetch(url);
620
- if (!response.ok) {
621
- throw new Error(`Stats request failed with ${response.status}`);
622
- }
623
- const data = await response.json();
624
-
625
- document.getElementById('totalCount').textContent = data.total;
626
- document.getElementById('avgImportance').textContent = data.avgImportance || '0';
627
- document.getElementById('totalHits').textContent = data.totalHitCount || '0';
628
- document.getElementById('expiringSoon').textContent = data.expiringSoon || '0';
629
- document.getElementById('codeFactCount').textContent = data.byType?.code_fact || 0;
630
- document.getElementById('decisionCount').textContent = data.byType?.decision || 0;
631
- document.getElementById('mistakeCount').textContent = data.byType?.mistake || 0;
632
- document.getElementById('patternCount').textContent = data.byType?.pattern || 0;
633
- const handoffCountEl = document.getElementById('handoffCount');
634
- const registeredCountEl = document.getElementById('registeredCount');
635
- if (handoffCountEl) handoffCountEl.textContent = data.byType?.agent_handoff || 0;
636
- if (registeredCountEl) registeredCountEl.textContent = data.byType?.agent_registered || 0;
637
-
638
- // Fill Task stats
639
- if (data.taskStats) {
640
- document.getElementById('totalTasks').textContent = data.taskStats.total || 0;
641
- document.getElementById('backlogTasksCount').textContent = data.taskStats.backlog || 0;
642
- document.getElementById('todoTasksCount').textContent = data.taskStats.todo || 0;
643
- document.getElementById('inProgressTasksCount').textContent = data.taskStats.inProgress || 0;
644
- document.getElementById('completedTasksCount').textContent = data.taskStats.completed || 0;
645
-
646
- // Today's Stats (Dashboard tab)
647
- if (document.getElementById('todayCompleted')) {
648
- document.getElementById('todayCompleted').textContent = data.todayCompleted || 0;
649
- }
650
- if (document.getElementById('todayAdded')) {
651
- document.getElementById('todayAdded').textContent = data.todayAdded || 0;
652
- }
653
- if (document.getElementById('todayProcessed')) {
654
- document.getElementById('todayProcessed').textContent = data.todayProcessed || 0;
655
- }
656
- if (document.getElementById('todayTokens')) {
657
- document.getElementById('todayTokens').textContent = formatTokens(data.todayTokens);
658
- }
659
- if (document.getElementById('todayAvgTime')) {
660
- document.getElementById('todayAvgTime').textContent = formatDuration(data.todayAvgDuration);
661
- }
662
-
663
- // Today's Stats (Tasks tab)
664
- if (document.getElementById('todayCompletedTasksCount')) {
665
- document.getElementById('todayCompletedTasksCount').textContent = data.todayCompleted || 0;
666
- }
667
- if (document.getElementById('todayAddedTasksCount')) {
668
- document.getElementById('todayAddedTasksCount').textContent = data.todayAdded || 0;
669
- }
670
- if (document.getElementById('todayProcessedTasksCount')) {
671
- document.getElementById('todayProcessedTasksCount').textContent = data.todayProcessed || 0;
672
- }
673
- if (document.getElementById('todayTokensTasksCount')) {
674
- document.getElementById('todayTokensTasksCount').textContent = formatTokens(data.todayTokens);
675
- }
676
- if (document.getElementById('todayAvgTimeTasksCount')) {
677
- document.getElementById('todayAvgTimeTasksCount').textContent = formatDuration(data.todayAvgDuration);
678
- }
679
-
680
- document.getElementById('backlogStatCount').textContent = data.taskStats.backlog || 0;
681
- document.getElementById('todoStatCount').textContent = data.taskStats.todo || 0;
682
- document.getElementById('inProgressStatCount').textContent = data.taskStats.inProgress || 0;
683
- document.getElementById('completedStatCount').textContent = data.taskStats.completed || 0;
684
- document.getElementById('blockedStatCount').textContent = data.taskStats.blocked || 0;
685
-
686
- // Also update column headers
687
- const backlogCountEl = document.getElementById('backlogCount');
688
- const todoCountEl = document.getElementById('todoCount');
689
- const inProgressCountEl = document.getElementById('inProgressCount');
690
- const completedCountEl = document.getElementById('completedCount');
691
-
692
- if (backlogCountEl) backlogCountEl.textContent = data.taskStats.backlog || 0;
693
- if (todoCountEl) todoCountEl.textContent = data.taskStats.todo || 0;
694
- if (inProgressCountEl) inProgressCountEl.textContent = data.taskStats.inProgress || 0;
695
- if (completedCountEl) completedCountEl.textContent = data.taskStats.completed || 0;
696
-
697
- updateTaskStatusChart(data.taskStats);
698
- }
699
- updateTypeChart(data.byType);
700
- updateTimeSeriesChart(data.timeSeries || {});
701
- updateScatterChart(data.scatterData || []);
702
- updateTopMemoriesChart(data.topMemories);
703
- syncStickyOffsets();
704
- } catch (err) {
705
- console.error('Failed to load stats:', err);
706
- }
707
- }
708
-
709
- function updateTypeChart(byType) {
710
- const ctx = document.getElementById('typeChart');
711
- if (!window.Chart || !ctx) return;
712
- if (charts.typeChart) charts.typeChart.destroy();
713
-
714
- const isDark = document.documentElement.classList.contains('dark');
715
- const counts = [
716
- byType?.decision || 0,
717
- byType?.mistake || 0,
718
- byType?.code_fact || 0,
719
- byType?.pattern || 0,
720
- byType?.agent_handoff || 0,
721
- byType?.agent_registered || 0
722
- ];
723
-
724
- charts.typeChart = new Chart(ctx, {
725
- type: 'doughnut',
726
- data: {
727
- labels: ['Decision', 'Mistake', 'Code Fact', 'Pattern', 'Handoff', 'Registration'],
728
- datasets: [{
729
- data: counts,
730
- backgroundColor: ['#fb7185', '#c084fc', '#38bdf8', '#34d399', '#fb923c', '#a3e635'],
731
- borderWidth: 2,
732
- borderColor: isDark ? '#1e293b' : '#ffffff',
733
- hoverOffset: 12
734
- }]
735
- },
736
- options: {
737
- responsive: true,
738
- maintainAspectRatio: false,
739
- cutout: '68%',
740
- plugins: {
741
- legend: { display: false },
742
- tooltip: {
743
- backgroundColor: isDark ? '#1e293b' : '#ffffff',
744
- titleColor: isDark ? '#f8fafc' : '#1e293b',
745
- bodyColor: isDark ? '#94a3b8' : '#64748b',
746
- borderColor: isDark ? '#334155' : '#e2e8f0',
747
- borderWidth: 1,
748
- padding: 10,
749
- cornerRadius: 10
750
- }
751
- }
752
- }
753
- });
754
- }
755
-
756
- function updateTaskStatusChart(taskStats) {
757
- const ctx = document.getElementById('taskStatusChart');
758
- if (!window.Chart || !ctx) return;
759
- if (charts.taskStatusChart) charts.taskStatusChart.destroy();
760
-
761
- const isDark = document.documentElement.classList.contains('dark');
762
- const counts = [
763
- taskStats?.backlog || 0,
764
- taskStats?.todo || 0,
765
- taskStats?.inProgress || 0,
766
- taskStats?.completed || 0,
767
- taskStats?.blocked || 0
768
- ];
769
-
770
- charts.taskStatusChart = new Chart(ctx, {
771
- type: 'doughnut',
772
- data: {
773
- labels: ['Backlog', 'To Do', 'In Progress', 'Completed', 'Blocked'],
774
- datasets: [{
775
- data: counts,
776
- backgroundColor: ['#64748b', '#94a3b8', '#38bdf8', '#10b981', '#fb7185'],
777
- borderWidth: 2,
778
- borderColor: isDark ? '#1e293b' : '#ffffff',
779
- hoverOffset: 12
780
- }]
781
- },
782
- options: {
783
- responsive: true,
784
- maintainAspectRatio: false,
785
- cutout: '68%',
786
- plugins: {
787
- legend: { display: false },
788
- tooltip: {
789
- backgroundColor: isDark ? '#1e293b' : '#ffffff',
790
- titleColor: isDark ? '#f8fafc' : '#1e293b',
791
- bodyColor: isDark ? '#94a3b8' : '#64748b',
792
- borderColor: isDark ? '#334155' : '#e2e8f0',
793
- borderWidth: 1,
794
- padding: 10,
795
- cornerRadius: 10
796
- }
797
- }
798
- }
799
- });
800
- }
801
-
802
- function updateTopMemoriesChart(memories = []) {
803
- const ctx = document.getElementById('topMemoriesChart');
804
- if (!window.Chart) {
805
- ctx.parentElement.innerHTML = '<div class="p-4 text-center text-gray-500 text-xs">Chart.js not available</div>';
806
- return;
807
- }
808
- if (charts.topMemoriesChart) charts.topMemoriesChart.destroy();
809
-
810
- const isDark = document.documentElement.classList.contains('dark');
811
- charts.topMemoriesChart = new Chart(ctx, {
812
- type: 'bar',
813
- data: {
814
- labels: memories.map((m, i) => `#${i + 1}`),
815
- datasets: [{
816
- label: 'Hit Count',
817
- data: memories.map(m => m.hit_count || m.importance),
818
- backgroundColor: isDark
819
- ? 'rgba(56,189,248,0.45)'
820
- : 'rgba(37,99,235,0.55)',
821
- borderColor: isDark ? '#38bdf8' : '#2563eb',
822
- borderWidth: 1,
823
- borderRadius: 6,
824
- borderSkipped: false
825
- }]
826
- },
827
- options: {
828
- responsive: true,
829
- maintainAspectRatio: false,
830
- plugins: {
831
- legend: { display: false },
832
- tooltip: {
833
- backgroundColor: isDark ? '#1e293b' : '#ffffff',
834
- titleColor: isDark ? '#f8fafc' : '#1e293b',
835
- bodyColor: isDark ? '#94a3b8' : '#64748b',
836
- borderColor: isDark ? '#334155' : '#e2e8f0',
837
- borderWidth: 1,
838
- padding: 8,
839
- cornerRadius: 8
840
- }
841
- },
842
- scales: {
843
- y: {
844
- beginAtZero: true,
845
- grid: { color: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.04)', drawBorder: false },
846
- ticks: { color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 } }
847
- },
848
- x: {
849
- grid: { display: false },
850
- ticks: { color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 } }
851
- }
852
- }
853
- }
854
- });
855
- }
856
-
857
- function updateTimeSeriesChart(timeSeries) {
858
- const ctx = document.getElementById('timeSeriesChart');
859
- if (!window.Chart || !ctx) return;
860
- if (charts.timeSeriesChart) charts.timeSeriesChart.destroy();
861
-
862
- const labels = Object.keys(timeSeries);
863
- const creationData = labels.map(k => (typeof timeSeries[k] === 'object' ? timeSeries[k].write : timeSeries[k]) || 0);
864
- const readData = labels.map(k => (typeof timeSeries[k] === 'object' ? timeSeries[k].read : 0) || 0);
865
- const searchData = labels.map(k => (typeof timeSeries[k] === 'object' ? timeSeries[k].search : 0) || 0);
866
-
867
- const isDark = document.documentElement.classList.contains('dark');
868
-
869
- charts.timeSeriesChart = new Chart(ctx, {
870
- type: 'line',
871
- data: {
872
- labels: labels.map(l => l.split('-').slice(1).join('/')),
873
- datasets: [
874
- {
875
- label: 'Created',
876
- data: creationData,
877
- borderColor: '#22d3ee',
878
- backgroundColor: 'rgba(34, 211, 238, 0.1)',
879
- borderWidth: 2,
880
- pointRadius: 3,
881
- tension: 0.4,
882
- fill: true
883
- },
884
- {
885
- label: 'Read',
886
- data: readData,
887
- borderColor: '#10b981',
888
- backgroundColor: 'transparent',
889
- borderWidth: 2,
890
- pointRadius: 3,
891
- tension: 0.4,
892
- borderDash: [5, 5]
893
- },
894
- {
895
- label: 'Search',
896
- data: searchData,
897
- borderColor: '#6366f1',
898
- backgroundColor: 'transparent',
899
- borderWidth: 2,
900
- pointRadius: 3,
901
- tension: 0.4,
902
- borderDash: [2, 2]
903
- }
904
- ]
905
- },
906
- options: {
907
- responsive: true,
908
- maintainAspectRatio: false,
909
- plugins: {
910
- legend: {
911
- position: 'top',
912
- align: 'end',
913
- labels: {
914
- color: isDark ? '#94a3b8' : '#64748b',
915
- usePointStyle: true,
916
- boxWidth: 6,
917
- font: { size: 10, weight: 'bold' }
918
- }
919
- }
920
- },
921
- scales: {
922
- y: {
923
- beginAtZero: true,
924
- grid: { color: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)', drawBorder: false },
925
- ticks: { color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 }, stepSize: 1 }
926
- },
927
- x: {
928
- grid: { display: false },
929
- ticks: { color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 }, maxRotation: 0 }
930
- }
931
- }
932
- }
933
- });
934
- }
935
-
936
- function showAddMemoryModal() {
937
- if (!currentRepo) {
938
- showToast('Please select a repository first', 'error');
939
- return;
940
- }
941
- document.getElementById('addMemoryModal').classList.remove('hidden');
942
- document.body.classList.add('drawer-open');
943
- }
944
-
945
- function hideAddMemoryModal() {
946
- document.getElementById('addMemoryModal').classList.add('hidden');
947
- document.body.classList.remove('drawer-open');
948
- document.getElementById('addMemoryForm').reset();
949
- }
950
-
951
- function showAddTaskModal() {
952
- if (!currentRepo) {
953
- showToast('Please select a repository first', 'error');
954
- return;
955
- }
956
- document.getElementById('addTaskModal').classList.remove('hidden');
957
- document.body.classList.add('drawer-open');
958
- }
959
-
960
- function hideAddTaskModal() {
961
- document.getElementById('addTaskModal').classList.add('hidden');
962
- document.body.classList.remove('drawer-open');
963
- document.getElementById('addTaskForm').reset();
964
- }
965
-
966
- async function handleMemorySubmit(event) {
967
- event.preventDefault();
968
- const formData = new FormData(event.target);
969
- const data = Object.fromEntries(formData.entries());
970
- data.repo = currentRepo;
971
- data.is_global = event.target.is_global.checked;
972
- // agent and model are already in data from FormData entries
973
-
974
- try {
975
- const res = await fetch('/api/memories', {
976
- method: 'POST',
977
- headers: { 'Content-Type': 'application/json' },
978
- body: JSON.stringify(data)
979
- });
980
- if (res.ok) {
981
- showToast('Memory added successfully', 'success');
982
- hideAddMemoryModal();
983
- loadData();
984
- } else {
985
- const err = await res.json();
986
- showToast(err.error || 'Failed to add memory', 'error');
987
- }
988
- } catch (err) {
989
- showToast('Network error', 'error');
990
- }
991
- }
992
-
993
- async function handleTaskSubmit(event) {
994
- event.preventDefault();
995
- const formData = new FormData(event.target);
996
- const data = Object.fromEntries(formData.entries());
997
- data.repo = currentRepo;
998
-
999
- try {
1000
- const res = await fetch('/api/tasks', {
1001
- method: 'POST',
1002
- headers: { 'Content-Type': 'application/json' },
1003
- body: JSON.stringify(data)
1004
- });
1005
- if (res.ok) {
1006
- showToast('Task created successfully', 'success');
1007
- hideAddTaskModal();
1008
- loadTasks();
1009
- loadRecentActions();
1010
- } else {
1011
- const err = await res.json();
1012
- showToast(err.error || 'Failed to create task', 'error');
1013
- }
1014
- } catch (err) {
1015
- showToast('Network error', 'error');
1016
- }
1017
- }
1018
-
1019
- window.showAddMemoryModal = showAddMemoryModal;
1020
- window.hideAddMemoryModal = hideAddMemoryModal;
1021
- window.showAddTaskModal = showAddTaskModal;
1022
- window.hideAddTaskModal = hideAddTaskModal;
1023
- window.handleMemorySubmit = handleMemorySubmit;
1024
- window.handleTaskSubmit = handleTaskSubmit;
1025
-
1026
- function updateScatterChart(scatterData) {
1027
- const ctx = document.getElementById('scatterChart');
1028
- if (!window.Chart) return;
1029
- if (charts.scatterChart) charts.scatterChart.destroy();
1030
-
1031
- const isDark = document.documentElement.classList.contains('dark');
1032
- charts.scatterChart = new Chart(ctx, {
1033
- type: 'scatter',
1034
- data: {
1035
- datasets: [{
1036
- label: 'Memories',
1037
- data: scatterData,
1038
- backgroundColor: isDark ? 'rgba(129,140,248,0.55)' : 'rgba(96,165,250,0.7)',
1039
- borderColor: isDark ? '#818cf8' : '#6366f1',
1040
- borderWidth: 1,
1041
- pointRadius: 5,
1042
- pointHoverRadius: 7
1043
- }]
1044
- },
1045
- options: {
1046
- responsive: true,
1047
- maintainAspectRatio: false,
1048
- plugins: {
1049
- legend: { display: false },
1050
- tooltip: {
1051
- backgroundColor: isDark ? '#1e293b' : '#ffffff',
1052
- titleColor: isDark ? '#f8fafc' : '#1e293b',
1053
- bodyColor: isDark ? '#94a3b8' : '#64748b',
1054
- borderColor: isDark ? '#334155' : '#e2e8f0',
1055
- borderWidth: 1,
1056
- padding: 8,
1057
- cornerRadius: 8
1058
- }
1059
- },
1060
- scales: {
1061
- x: {
1062
- title: { display: true, text: 'Importance', color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 } },
1063
- min: 0, max: 6,
1064
- grid: { color: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.04)' },
1065
- ticks: { color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 } }
1066
- },
1067
- y: {
1068
- title: { display: true, text: 'Hit Count', color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 } },
1069
- beginAtZero: true,
1070
- grid: { color: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.04)' },
1071
- ticks: { color: isDark ? '#64748b' : '#94a3b8', font: { size: 10 } }
1072
- }
1073
- }
1074
- }
1075
- });
1076
- }
1077
-
1078
- async function loadMemories() {
1079
- if (!currentRepo) {
1080
- document.getElementById('tableContainer').innerHTML = '<div class="text-gray-500 py-12">Please select a repository</div>';
1081
- return;
1082
- }
1083
-
1084
- try {
1085
- renderTableSkeleton();
1086
- const typeFilter = document.getElementById('typeFilter').value;
1087
- const search = document.getElementById('searchInput').value.trim();
1088
- const minImportance = document.getElementById('minImportanceFilter').value;
1089
- const maxImportance = document.getElementById('maxImportanceFilter').value;
1090
-
1091
- let url = `/api/memories?repo=${encodeURIComponent(currentRepo)}&page=${currentPage}&pageSize=${pageSize}&sortBy=${encodeURIComponent(sortColumn)}&sortOrder=${encodeURIComponent(sortOrder)}`;
1092
- if (typeFilter) url += `&type=${typeFilter}`;
1093
- if (search) url += `&search=${encodeURIComponent(search)}`;
1094
- if (minImportance) url += `&minImportance=${encodeURIComponent(minImportance)}`;
1095
- if (maxImportance) url += `&maxImportance=${encodeURIComponent(maxImportance)}`;
1096
-
1097
- const response = await fetch(url);
1098
- const data = await response.json();
1099
- if (!response.ok) throw new Error(data.error || 'Failed to load memories');
1100
- currentMemories = data.memories;
1101
- currentPaginatedData = currentMemories;
1102
- totalItems = data.pagination?.totalItems || currentMemories.length;
1103
- totalPages = data.pagination?.totalPages || 1;
1104
- updatePaginationControls(totalItems);
1105
- renderTable(currentMemories);
1106
- } catch (err) {
1107
- console.error('Failed to load memories:', err);
1108
- document.getElementById('errorMessage').innerHTML = `<div class="p-4 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300 rounded mb-4">Failed to load memories: ${err.message}</div>`;
1109
- }
1110
- }
1111
-
1112
- function renderTableSkeleton() {
1113
- const container = document.getElementById('tableContainer');
1114
- container.innerHTML = `
1115
- <div class="overflow-x-auto max-h-[68vh]">
1116
- <table class="w-full border-collapse sticky-table-header table-animate">
1117
- <thead>
1118
- <tr class="border-b-2 border-gray-200 dark:border-gray-700">
1119
- <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold"></th>
1120
- <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Memory</th>
1121
- <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Source</th>
1122
- <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Type</th>
1123
- <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Priority</th>
1124
- <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Usage</th>
1125
- <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Freshness</th>
1126
- <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold sticky-actions">Actions</th>
1127
- </tr>
1128
- </thead>
1129
- <tbody>
1130
- ${Array.from({ length: Math.min(pageSize, 6) }).map(() => `
1131
- <tr class="border-b border-gray-100 dark:border-gray-700">
1132
- <td class="p-3"><div class="skeleton h-4 w-4"></div></td>
1133
- <td class="p-3"><div class="skeleton h-4 w-52 mb-2"></div><div class="skeleton h-3 w-36"></div></td>
1134
- <td class="p-3"><div class="skeleton h-4 w-20 mb-2"></div><div class="skeleton h-3 w-16"></div></td>
1135
- <td class="p-3"><div class="skeleton h-6 w-20"></div></td>
1136
- <td class="p-3"><div class="skeleton h-6 w-12 mb-2"></div><div class="skeleton h-3 w-12"></div></td>
1137
- <td class="p-3"><div class="skeleton h-4 w-16 mb-2"></div><div class="skeleton h-3 w-14"></div></td>
1138
- <td class="p-3"><div class="skeleton h-4 w-14 mb-2"></div><div class="skeleton h-3 w-12"></div></td>
1139
- <td class="p-3 sticky-actions"><div class="flex gap-2"><div class="skeleton h-7 w-14"></div><div class="skeleton h-7 w-14"></div></div></td>
1140
- </tr>
1141
- `).join('')}
1142
- </tbody>
1143
- </table>
1144
- </div>
1145
- `;
1146
- }
1147
-
1148
- function escapeHtml(text) {
1149
- const div = document.createElement('div');
1150
- div.textContent = text;
1151
- return div.innerHTML;
1152
- }
1153
-
1154
- function renderMarkdown(text) {
1155
- if (!text) return '';
1156
- if (typeof marked === 'undefined') {
1157
- // Fallback if marked.js didn't load
1158
- const div = document.createElement('div');
1159
- div.textContent = text;
1160
- return `<pre style="white-space:pre-wrap;font-size:0.85rem;line-height:1.7">${div.innerHTML}</pre>`;
1161
- }
1162
- try {
1163
- marked.setOptions({ breaks: true, gfm: true });
1164
- return marked.parse(text);
1165
- } catch (e) {
1166
- const div = document.createElement('div');
1167
- div.textContent = text;
1168
- return `<pre style="white-space:pre-wrap">${div.innerHTML}</pre>`;
1169
- }
1170
- }
1171
-
1172
- function formatDate(dateStr) {
1173
- const date = new Date(dateStr);
1174
- const now = new Date();
1175
- const diff = Math.floor((now - date) / 1000);
1176
- if (diff < 60) return `${diff}s ago`;
1177
- if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
1178
- if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
1179
- return `${Math.floor(diff / 86400)}d ago`;
1180
- }
1181
-
1182
- function renderTable(memories) {
1183
- const container = document.getElementById('tableContainer');
1184
- const searchQuery = document.getElementById('searchInput').value.toLowerCase();
1185
- const paginated = memories;
1186
-
1187
- if (paginated.length === 0) {
1188
- container.innerHTML = '<div class="text-gray-500 py-12">No memories found matching your filters</div>';
1189
- return;
1190
- }
1191
-
1192
- function highlightText(text, query) {
1193
- if (!query) return escapeHtml(text);
1194
- const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
1195
- return escapeHtml(text).replace(regex, '<mark>$1</mark>');
1196
- }
1197
-
1198
- const table = document.createElement('div');
1199
- table.className = 'overflow-x-auto max-h-[68vh]';
1200
- table.innerHTML = `
1201
- <table class="w-full border-collapse sticky-table-header table-animate">
1202
- <thead>
1203
- <tr class="border-b-2 border-gray-200 dark:border-gray-700">
1204
- <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold"><input type="checkbox" id="selectAll" onchange="toggleSelectAll()"></th>
1205
- <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>
1206
- <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>
1207
- <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>
1208
-
1209
- <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>
1210
- <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>
1211
- <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>
1212
- <th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Actions</th>
1213
- </tr>
1214
- </thead>
1215
- <tbody>
1216
- ${paginated.map(m => `
1217
- <tr id="row-${m.id}" class="border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 ${m.status === 'archived' ? 'opacity-60 grayscale-[0.5]' : ''}">
1218
- <td class="p-3"><input type="checkbox" class="row-checkbox" value="${m.id}" ${selectedIds.has(m.id) ? 'checked' : ''} onchange="toggleSelect('${m.id}')"></td>
1219
- <td class="p-3 min-w-[18rem]">
1220
- <div class="flex items-center gap-2">
1221
- <button onclick="openDrawer('${m.id}')" class="text-left font-semibold text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 transition-colors">${highlightText(getDisplayTitle(m), searchQuery)}</button>
1222
- ${m.is_global ? '<span class="px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-900/40 text-[10px] font-bold text-amber-700 dark:text-amber-300 uppercase">Global</span>' : ''}
1223
- ${m.status === 'archived' ? '<span class="px-1.5 py-0.5 rounded bg-gray-200 dark:bg-gray-800 text-[10px] font-bold text-gray-500 uppercase">Archived</span>' : ''}
1224
- </div>
1225
- <div class="mt-1 flex flex-wrap gap-1">
1226
- ${m.tags && Array.isArray(m.tags) ? m.tags.map(tag => `<span class="text-[9px] px-1 bg-gray-100 dark:bg-gray-800 text-gray-500 rounded border border-gray-200 dark:border-gray-700">#${tag}</span>`).join('') : ''}
1227
- </div> <div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
1228
- <span class="font-mono">${m.id.substring(0, 8)}</span>
1229
- <span class="mx-1">•</span>
1230
- <span>${m.scope?.repo || 'Unknown repo'}</span>
1231
- </div>
1232
- </td>
1233
- <td class="p-3">
1234
- <div class="flex flex-col">
1235
- <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>
1236
- <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>
1237
- <span class="text-[9px] text-gray-400 dark:text-gray-500 truncate max-w-[100px]" title="${m.model || 'unknown'}">${m.model || 'unknown'}</span>
1238
- </div>
1239
- </td>
1240
- <td class="p-3"><span class="table-chip type-${m.type}">${formatTypeLabel(m.type)}</span></td>
1241
- <td class="p-3">
1242
- <div class="metric-badge ${getImportanceBadgeClass(m.importance)}">${m.importance}/5</div>
1243
- <div class="mt-1 text-xs text-gray-500 dark:text-gray-400">${getImportanceLabel(m.importance)}</div>
1244
- </td>
1245
- <td class="p-3">
1246
- <div class="font-semibold">${formatUsageCount(m.hit_count)}</div>
1247
- <div class="mt-1 text-xs text-gray-500 dark:text-gray-400">${formatRecallRate(m.recall_rate)}</div>
1248
- </td>
1249
- <td class="p-3">
1250
- <div class="font-medium">${formatDate(m.created_at)}</div>
1251
- <div class="mt-1 text-xs text-gray-500 dark:text-gray-400">Updated ${formatDate(m.updated_at)}</div>
1252
- </td>
1253
- <td class="p-3 sticky-actions">
1254
- <div class="flex flex-wrap gap-1.5">
1255
- <button onclick="openDrawer('${m.id}')" class="btn-open">Open</button>
1256
- <button onclick="startInlineEdit('${m.id}')" class="btn-edit-light">Edit</button>
1257
- </div>
1258
- </td>
1259
- </tr>
1260
- `).join('')}
1261
- </tbody>
1262
- </table>
1263
- `;
1264
-
1265
- container.innerHTML = '';
1266
- container.appendChild(table);
1267
- }
1268
-
1269
- let searchDebounce = null;
1270
- document.getElementById('searchInput').addEventListener('input', () => {
1271
- if (searchDebounce) clearTimeout(searchDebounce);
1272
- searchDebounce = setTimeout(() => {
1273
- currentPage = 1;
1274
- loadMemories();
1275
- }, 300);
1276
- });
1277
-
1278
- ['typeFilter', 'minImportanceFilter', 'maxImportanceFilter'].forEach(id => {
1279
- document.getElementById(id).addEventListener('change', () => {
1280
- currentPage = 1;
1281
- loadMemories();
1282
- });
1283
- });
1284
-
1285
- function changePageSize() {
1286
- pageSize = parseInt(document.getElementById('pageSizeSelect').value);
1287
- currentPage = 1;
1288
- loadMemories();
1289
- }
1290
-
1291
- function goToPage(page) {
1292
- if (page < 1 || page > totalPages) return;
1293
- currentPage = page;
1294
- loadMemories();
1295
- }
1296
-
1297
- function updatePaginationControls(totalItems) {
1298
- totalPages = Math.max(1, totalPages || Math.ceil(totalItems / pageSize) || 1);
1299
- if (currentPage > totalPages) currentPage = totalPages;
1300
-
1301
- document.getElementById('firstPageBtn').disabled = currentPage <= 1;
1302
- document.getElementById('prevPageBtn').disabled = currentPage <= 1;
1303
- document.getElementById('nextPageBtn').disabled = currentPage >= totalPages;
1304
- document.getElementById('lastPageBtn').disabled = currentPage >= totalPages;
1305
-
1306
- const start = totalItems > 0 ? ((currentPage - 1) * pageSize) + 1 : 0;
1307
- const end = Math.min(currentPage * pageSize, totalItems);
1308
- document.getElementById('paginationInfo').textContent = totalItems > 0 ? `Showing ${start}-${end} of ${totalItems}` : 'No results';
1309
- }
1310
-
1311
- let sortColumn = 'hit_count';
1312
- let sortOrder = 'desc';
1313
-
1314
- function formatTypeLabel(type) {
1315
- return type.replace('_', ' ');
1316
- }
1317
-
1318
- function getDisplayTitle(memory) {
1319
- if (memory.title && memory.title.trim()) {
1320
- return memory.title.trim();
1321
- }
1322
- return memory.content.length > 80 ? `${memory.content.substring(0, 77)}...` : memory.content;
1323
- }
1324
-
1325
- function getContentPreview(memory) {
1326
- const text = memory.content.replace(/\s+/g, ' ').trim();
1327
- return text.length > 220 ? `${text.substring(0, 217)}...` : text;
1328
- }
1329
-
1330
- function getImportanceLabel(importance) {
1331
- if (importance >= 5) return 'Critical';
1332
- if (importance >= 4) return 'High';
1333
- if (importance >= 3) return 'Medium';
1334
- if (importance >= 2) return 'Low';
1335
- return 'Minor';
1336
- }
1337
-
1338
- function getImportanceBadgeClass(importance) {
1339
- 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';
1340
- 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';
1341
- 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';
1342
- 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';
1343
- return 'bg-gradient-to-r from-slate-300/10 to-gray-300/10 text-slate-600 dark:text-slate-400 border-slate-300/30';
1344
- }
1345
-
1346
- function formatUsageCount(hitCount) {
1347
- const value = hitCount || 0;
1348
- if (value === 0) return 'Unused';
1349
- if (value === 1) return '1 hit';
1350
- return `${value} hits`;
1351
- }
1352
-
1353
- function formatRecallRate(recallRate) {
1354
- if (!recallRate) return 'Not recalled yet';
1355
- return `${(recallRate * 100).toFixed(1)}% recall`;
1356
- }
1357
-
1358
- function sortTable(column) {
1359
- if (sortColumn === column) {
1360
- sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
1361
- } else {
1362
- sortColumn = column;
1363
- sortOrder = 'desc';
1364
- }
1365
-
1366
- document.querySelectorAll('th[data-sort]').forEach(th => {
1367
- th.classList.remove('sorted-asc', 'sorted-desc');
1368
- if (th.dataset.sort === column) {
1369
- th.classList.add(sortOrder === 'asc' ? 'sorted-asc' : 'sorted-desc');
1370
- }
1371
- });
1372
-
1373
- currentPage = 1;
1374
- loadMemories();
1375
- }
1376
-
1377
- document.querySelectorAll('th[data-sort]').forEach(th => {
1378
- if (th.dataset.sort === sortColumn) {
1379
- th.classList.add(sortOrder === 'asc' ? 'sorted-asc' : 'sorted-desc');
1380
- }
1381
- });
1382
-
1383
- function toggleSelectAll() {
1384
- const checked = document.getElementById('selectAll').checked;
1385
- currentPaginatedData.forEach(m => {
1386
- if (checked) selectedIds.add(m.id);
1387
- else selectedIds.delete(m.id);
1388
- });
1389
- updateBulkBar();
1390
- renderTable(currentMemories);
1391
- }
1392
-
1393
- function toggleSelect(id) {
1394
- if (selectedIds.has(id)) selectedIds.delete(id);
1395
- else selectedIds.add(id);
1396
- updateBulkBar();
1397
- }
1398
-
1399
- function updateBulkBar() {
1400
- const bar = document.getElementById('bulkActionBar');
1401
- if (selectedIds.size > 0) {
1402
- bar.classList.remove('hidden');
1403
- document.getElementById('selectedCount').textContent = `${selectedIds.size} selected`;
1404
- } else {
1405
- bar.classList.add('hidden');
1406
- }
1407
- }
1408
-
1409
- function clearSelection() {
1410
- selectedIds.clear();
1411
- updateBulkBar();
1412
- renderTable(currentMemories);
1413
- }
1414
-
1415
- function renderDetailPanel(data) {
1416
- const isEditing = activeEditMemoryId === data.id;
1417
- return `
1418
- <div class="space-y-5">
1419
- <div class="rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/40 p-4">
1420
- <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-2">Summary</div>
1421
- <p class="text-sm leading-6 text-gray-700 dark:text-gray-300">${escapeHtml(getContentPreview(data))}</p>
1422
- </div>
1423
- <div class="grid gap-4 md:grid-cols-3">
1424
- <div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
1425
- <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">Memory Info</div>
1426
- <div class="space-y-2 text-sm">
1427
- <div><strong>Type:</strong> ${formatTypeLabel(data.type)}</div>
1428
- <div><strong>ID:</strong> <span class="font-mono text-[10px]">${data.id}</span></div>
1429
- <div><strong>Priority:</strong> ${data.importance}/5</div>
1430
- <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>
1431
- </div>
1432
- </div>
1433
- <div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
1434
- <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">Source Info</div>
1435
- <div class="space-y-2 text-sm">
1436
- <div><strong>Agent:</strong> ${escapeHtml(data.agent || 'unknown')}</div>
1437
- <div><strong>Role:</strong> ${escapeHtml(data.role || 'unknown')}</div>
1438
- <div><strong>Model:</strong> ${escapeHtml(data.model || 'unknown')}</div>
1439
- <div><strong>Repo:</strong> ${escapeHtml(data.scope?.repo || 'N/A')}</div>
1440
- </div>
1441
- </div>
1442
- <div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
1443
- <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">Usage</div>
1444
- <div class="space-y-2 text-sm">
1445
- <div><strong>Hit Count:</strong> ${data.hit_count || 0}</div>
1446
- <div><strong>Recall Rate:</strong> ${formatRecallRate(data.recall_rate)}</div>
1447
- <div><strong>Last Used:</strong> ${data.last_used_at ? new Date(data.last_used_at).toLocaleDateString() : 'Never'}</div>
1448
- </div>
1449
- </div>
1450
- </div>
1451
-
1452
- ${data.type === 'agent_registered' ? `
1453
- <div class="rounded-xl border border-lime-200 dark:border-lime-900 bg-lime-50 dark:bg-lime-900/20 p-4">
1454
- <div class="text-xs uppercase tracking-wide text-lime-600 dark:text-lime-400 mb-2">Agent Status</div>
1455
- <div class="flex items-center gap-2">
1456
- <span class="inline-flex items-center px-2 py-1 rounded text-xs font-bold uppercase ${data.status === 'active' ? 'bg-lime-500 text-white' : 'bg-gray-400 text-white'}">
1457
- ${escapeHtml(data.status)}
1458
- </span>
1459
- </div>
1460
- </div>
1461
- ` : ''}
1462
-
1463
- ${data.type === 'agent_handoff' ? `
1464
- <div class="rounded-xl border border-orange-200 dark:border-orange-900 bg-orange-50 dark:bg-orange-900/20 p-4">
1465
- <div class="text-xs uppercase tracking-wide text-orange-600 dark:text-orange-400 mb-2">Handoff Details</div>
1466
- <div class="space-y-2 text-sm">
1467
- <div><strong>Completed at:</strong> ${data.completed_at ? new Date(data.completed_at).toLocaleString() : 'Pending'}</div>
1468
- <div><strong>Task Status:</strong> <span class="capitalize px-1.5 py-0.5 rounded bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300">${data.status}</span></div>
1469
- </div>
1470
- </div>
1471
- ` : ''}
1472
-
1473
- ${data.supersedes ? `
1474
- <div class="rounded-xl border border-blue-200 dark:border-blue-900 bg-blue-50 dark:bg-blue-900/20 p-4">
1475
- <div class="text-xs uppercase tracking-wide text-blue-500 dark:text-blue-400 mb-2">Supersedes</div>
1476
- <div class="flex items-center gap-2">
1477
- <svg class="w-4 h-4 text-blue-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>
1478
- <button onclick="openDrawer('${data.supersedes}')" class="text-sm font-mono text-blue-600 dark:text-blue-400 hover:underline">Replaced memory ${data.supersedes.substring(0, 8)}…</button>
1479
- </div>
1480
- </div>
1481
- ` : ''}
1482
- <div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
1483
- <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">Timeline</div>
1484
- <div class="space-y-2 text-sm">
1485
- <div><strong>Created:</strong> ${new Date(data.created_at).toLocaleString()}</div>
1486
- <div><strong>Updated:</strong> ${data.updated_at ? new Date(data.updated_at).toLocaleString() : 'N/A'}</div>
1487
- <div><strong>Expires:</strong> ${data.expires_at ? new Date(data.expires_at).toLocaleString() : 'Never'}</div>
1488
- </div>
1489
- </div>
1490
- <div class="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
1491
- <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">
1492
- <div class="text-xs uppercase tracking-wide font-bold text-gray-500 dark:text-gray-400">Full Content</div>
1493
- <span class="text-[10px] text-gray-400 dark:text-gray-500 flex items-center gap-1">
1494
- <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>
1495
- Markdown
1496
- </span>
1497
- </div>
1498
- <div class="p-4 md:p-5 markdown-body">${renderMarkdown(data.content)}</div>
1499
- </div>
1500
- <div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
1501
- <div class="flex items-center justify-between mb-3">
1502
- <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">Quick Edit</div>
1503
- ${isEditing
1504
- ? '<button onclick="cancelDrawerEdit()" class="text-xs px-3 py-1.5 rounded bg-gray-200 dark:bg-gray-700">Cancel</button>'
1505
- : `<button onclick="startDrawerEdit('${data.id}')" class="text-xs px-3 py-1.5 rounded bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200">Edit</button>`}
1506
- </div>
1507
- ${isEditing ? renderDrawerEditForm(data) : `<p class="text-sm text-gray-500 dark:text-gray-400">Edit title, content, and priority from this panel without leaving the current table context.</p>`}
1508
- </div>
1509
- </div>
1510
- `;
1511
- }
1512
-
1513
- function renderDrawerEditForm(data) {
1514
- return `
1515
- <div class="space-y-3">
1516
- <input id="drawer-edit-title" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700" value="${escapeHtml(data.title || '')}" placeholder="Memory title">
1517
- <textarea id="drawer-edit-content" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700" rows="6">${escapeHtml(data.content)}</textarea>
1518
- <div class="flex items-center gap-3">
1519
- <label class="text-sm text-gray-600 dark:text-gray-400">Priority</label>
1520
- <input type="number" id="drawer-edit-importance" value="${data.importance}" min="1" max="5" class="w-20 p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
1521
- </div>
1522
- <div class="flex gap-2">
1523
- <button onclick="saveDrawerEdit('${data.id}')" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">Save</button>
1524
- <button onclick="cancelDrawerEdit()" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded text-sm">Cancel</button>
1525
- </div>
1526
- </div>
1527
- `;
1528
- }
1529
-
1530
- async function openDrawer(id) {
1531
- try {
1532
- currentDrawerMemoryId = id;
1533
- document.getElementById('drawerTitle').textContent = 'Loading...';
1534
- document.getElementById('drawerBody').innerHTML = `
1535
- <div class="space-y-4">
1536
- <div class="skeleton h-20 w-full"></div>
1537
- <div class="grid gap-4 md:grid-cols-2">
1538
- <div class="skeleton h-36 w-full"></div>
1539
- <div class="skeleton h-36 w-full"></div>
1540
- </div>
1541
- <div class="skeleton h-24 w-full"></div>
1542
- <div class="skeleton h-64 w-full"></div>
1543
- </div>
1544
- `;
1545
- document.getElementById('memoryDrawer').classList.remove('hidden');
1546
- document.body.classList.add('drawer-open');
1547
-
1548
- // Trigger slide-in animation
1549
- setTimeout(() => {
1550
- const aside = document.getElementById('drawerAside');
1551
- if (aside) {
1552
- aside.classList.remove('translate-x-full');
1553
- aside.classList.add('translate-x-0');
1554
- }
1555
- }, 10);
1556
-
1557
- const response = await fetch(`/api/memories/${id}?repo=${encodeURIComponent(currentRepo)}`);
1558
- const data = await response.json();
1559
- if (!response.ok) throw new Error(data.error || 'Failed to load memory');
1560
- document.getElementById('drawerTitle').textContent = getDisplayTitle(data);
1561
- document.getElementById('drawerBody').innerHTML = renderDetailPanel(data);
1562
- } catch (err) {
1563
- closeDrawer();
1564
- showToast('Failed to load details: ' + err.message, 'error');
1565
- }
1566
- }
1567
-
1568
- function closeDrawer() {
1569
- const aside = document.getElementById('drawerAside');
1570
- if (aside) {
1571
- aside.classList.remove('translate-x-0');
1572
- aside.classList.add('translate-x-full');
1573
- }
1574
-
1575
- // Delay hiding the container until animation completes
1576
- setTimeout(() => {
1577
- document.getElementById('memoryDrawer').classList.add('hidden');
1578
- if (!document.getElementById('repoSidebarDrawer') || document.getElementById('repoSidebarDrawer').classList.contains('hidden')) {
1579
- document.body.classList.remove('drawer-open');
1580
- }
1581
- }, 500);
1582
-
1583
- activeEditMemoryId = null;
1584
- currentDrawerMemoryId = null;
1585
- }
1586
-
1587
- function startDrawerEdit(id) {
1588
- activeEditMemoryId = id;
1589
- openDrawer(id);
1590
- }
1591
-
1592
- function cancelDrawerEdit() {
1593
- const id = currentDrawerMemoryId;
1594
- activeEditMemoryId = null;
1595
- if (id) {
1596
- openDrawer(id);
1597
- } else {
1598
- closeDrawer();
1599
- }
1600
- }
1601
-
1602
- async function saveDrawerEdit(id) {
1603
- const title = document.getElementById('drawer-edit-title').value.trim();
1604
- const content = document.getElementById('drawer-edit-content').value;
1605
- const importance = parseInt(document.getElementById('drawer-edit-importance').value, 10);
1606
-
1607
- try {
1608
- const response = await fetch(`/api/memories/${id}?repo=${encodeURIComponent(currentRepo)}`, {
1609
- method: 'PUT',
1610
- headers: { 'Content-Type': 'application/json' },
1611
- body: JSON.stringify({ title, content, importance })
1612
- });
1613
-
1614
- if (!response.ok) {
1615
- const data = await response.json();
1616
- throw new Error(data.error || 'Failed to update');
1617
- }
1618
-
1619
- showToast('Memory updated successfully', 'success');
1620
- activeEditMemoryId = null;
1621
- await Promise.all([loadMemories(), loadStats(), loadRecentActions()]);
1622
- await openDrawer(id);
1623
- } catch (err) {
1624
- showToast('Failed to update: ' + err.message, 'error');
1625
- }
1626
- }
1627
-
1628
- async function startInlineEdit(id) {
1629
- activeEditMemoryId = id;
1630
- await openDrawer(id);
1631
- }
1632
-
1633
- async function saveInlineEdit(id) {
1634
- await saveDrawerEdit(id);
1635
- }
1636
-
1637
- function cancelInlineEdit(id) {
1638
- cancelDrawerEdit();
1639
- }
1640
-
1641
- async function bulkUpdateImportance() {
1642
- const importance = parseInt(document.getElementById('bulkImportanceSelect').value);
1643
- const ids = Array.from(selectedIds);
1644
-
1645
- try {
1646
- await Promise.all(ids.map(id =>
1647
- fetch(`/api/memories/${id}?repo=${encodeURIComponent(currentRepo)}`, {
1648
- method: 'PUT',
1649
- headers: { 'Content-Type': 'application/json' },
1650
- body: JSON.stringify({ importance })
1651
- })
1652
- ));
1653
-
1654
- showToast(`Updated ${ids.length} memories`, 'success');
1655
- clearSelection();
1656
- loadData();
1657
- } catch (err) {
1658
- showToast('Bulk update failed: ' + err.message, 'error');
1659
- }
1660
- }
1661
-
1662
- async function showBulkDeleteConfirm() {
1663
- if (!confirm(`Delete ${selectedIds.size} selected memories? This cannot be undone.`)) return;
1664
-
1665
- const ids = Array.from(selectedIds);
1666
- try {
1667
- await Promise.all(ids.map(id =>
1668
- fetch(`/api/memories/${id}?repo=${encodeURIComponent(currentRepo)}`, { method: 'DELETE' })
1669
- ));
1670
-
1671
- showToast(`Deleted ${ids.length} memories`, 'success');
1672
- clearSelection();
1673
- loadData();
1674
- } catch (err) {
1675
- showToast('Bulk delete failed: ' + err.message, 'error');
1676
- }
1677
- }
1678
-
1679
- function showToast(message, type = 'info') {
1680
- const container = document.getElementById('toastContainer');
1681
- const toast = document.createElement('div');
1682
- const colors = { success: 'bg-green-600', error: 'bg-red-600', info: 'bg-blue-600' };
1683
- toast.className = `${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg max-w-xs`;
1684
- toast.textContent = message;
1685
- container.appendChild(toast);
1686
-
1687
- setTimeout(() => {
1688
- toast.classList.add('removing');
1689
- setTimeout(() => toast.remove(), 300);
1690
- }, 3000);
1691
- }
1692
-
1693
- function exportData(format) {
1694
- const data = currentMemories.map(m => ({
1695
- id: m.id,
1696
- type: m.type,
1697
- content: m.content,
1698
- importance: m.importance,
1699
- hit_count: m.hit_count,
1700
- created_at: m.created_at
1701
- }));
1702
-
1703
- let content, filename, type;
1704
- if (format === 'json') {
1705
- content = JSON.stringify(data, null, 2);
1706
- filename = 'memories.json';
1707
- type = 'application/json';
1708
- } else {
1709
- const headers = ['id', 'type', 'content', 'importance', 'hit_count', 'created_at'];
1710
- content = [headers.join(','), ...data.map(m => headers.map(h => `"${(m[h] || '').toString().replace(/"/g, '""')}"`).join(','))].join('\n');
1711
- filename = 'memories.csv';
1712
- type = 'text/csv';
1713
- }
1714
-
1715
- const blob = new Blob([content], { type });
1716
- const url = URL.createObjectURL(blob);
1717
- const a = document.createElement('a');
1718
- a.href = url;
1719
- a.download = filename;
1720
- a.click();
1721
- URL.revokeObjectURL(url);
1722
- showToast(`Exported as ${format.toUpperCase()}`, 'success');
1723
- }
1724
-
1725
- async function archiveExpired() {
1726
- try {
1727
- const response = await fetch('/api/archive-expired', { method: 'POST' });
1728
- const data = await response.json();
1729
- showToast(`Archived ${data.archived || 0} expired memories`, 'success');
1730
- loadData();
1731
- } catch (err) {
1732
- showToast('Archive failed: ' + err.message, 'error');
1733
- }
1734
- }
1735
-
1736
- function exportHandbook() {
1737
- if (!currentRepo) {
1738
- showToast('Please select a repository first', 'info');
1739
- return;
1740
- }
1741
- window.location.href = `/api/export/handbook/${encodeURIComponent(currentRepo)}`;
1742
- }
1743
-
1744
- async function loadData() {
1745
- await loadRepos();
1746
- await Promise.all([
1747
- loadStats(),
1748
- loadMemories(),
1749
- loadTasks(),
1750
- checkStatus(),
1751
- loadRecentActions(),
1752
- ]);
1753
- scheduleTabIndicatorPosition(currentTab);
1754
- }
1755
-
1756
- let currentTasks = [];
1757
-
1758
- async function loadTasks() {
1759
- if (!currentRepo) return;
1760
-
1761
- taskTimeStats = null; // Force reload
1762
- updateTimeStats('daily');
1763
-
1764
- // Reset pagination
1765
- taskPagination.backlog = { page: 1, pageSize: 20, hasMore: true, loading: false };
1766
- taskPagination.todo = { page: 1, pageSize: 20, hasMore: true, loading: false };
1767
- taskPagination.in_progress = { page: 1, pageSize: 20, hasMore: true, loading: false };
1768
- taskPagination.completed = { page: 1, pageSize: 20, hasMore: true, loading: false };
1769
-
1770
- // Clear containers
1771
- document.getElementById('backlogTasks').innerHTML = '';
1772
- document.getElementById('todoTasks').innerHTML = '';
1773
- document.getElementById('inProgressTasks').innerHTML = '';
1774
- document.getElementById('completedTasks').innerHTML = '';
1775
-
1776
- await Promise.all([
1777
- loadTaskCategory('backlog'),
1778
- loadTaskCategory('pending,blocked,canceled'),
1779
- loadTaskCategory('in_progress'),
1780
- loadTaskCategory('completed')
1781
- ]);
1782
-
1783
- setupTaskScrollListeners();
1784
- }
1785
-
1786
- async function loadTaskCategory(status) {
1787
- const category = status === 'backlog' ? 'backlog' : ((status.includes('pending') || status.includes('blocked') || status.includes('canceled')) ? 'todo' : (status === 'in_progress' ? 'in_progress' : 'completed'));
1788
- const pag = taskPagination[category];
1789
-
1790
- if (!pag.hasMore || pag.loading) return;
1791
-
1792
- pag.loading = true;
1793
- const containerId = { backlog: 'backlogTasks', todo: 'todoTasks', in_progress: 'inProgressTasks', completed: 'completedTasks' }[category];
1794
- const container = document.getElementById(containerId);
1795
-
1796
- // Show loading indicator
1797
- const loadingId = `loading-${category}`;
1798
- if (!document.getElementById(loadingId)) {
1799
- const loader = document.createElement('div');
1800
- loader.id = loadingId;
1801
- loader.className = 'py-4 text-center text-gray-400 text-[10px] animate-pulse w-full';
1802
- loader.textContent = 'Loading more...';
1803
- container.appendChild(loader);
1804
- }
1805
-
1806
- try {
1807
- const searchInput = document.getElementById('taskSearchInput');
1808
- const searchTerm = searchInput ? encodeURIComponent(searchInput.value.trim()) : '';
1809
- const response = await fetch(`/api/tasks?repo=${encodeURIComponent(currentRepo)}&status=${status}&page=${pag.page}&pageSize=${pag.pageSize}&search=${searchTerm}`);
1810
- const data = await response.json();
1811
-
1812
- const tasks = data.tasks || [];
1813
-
1814
- // Remove loader
1815
- const loader = document.getElementById(loadingId);
1816
- if (loader) loader.remove();
1817
-
1818
- if (tasks.length < pag.pageSize) {
1819
- pag.hasMore = false;
1820
- }
1821
-
1822
- renderTaskCards(containerId, tasks, pag.page === 1);
1823
- pag.page++;
1824
- } catch (err) {
1825
- console.error(`Failed to load ${category} tasks:`, err);
1826
- } finally {
1827
- pag.loading = false;
1828
- }
1829
- }
1830
-
1831
- function setupTaskScrollListeners() {
1832
- ['backlogTasks', 'todoTasks', 'inProgressTasks', 'completedTasks'].forEach(id => {
1833
- const el = document.getElementById(id);
1834
- if (!el) return;
1835
-
1836
- el.onscroll = () => {
1837
- if (el.scrollTop + el.clientHeight >= el.scrollHeight - 50) {
1838
- const category = id === 'backlogTasks' ? 'backlog' : (id === 'todoTasks' ? 'todo' : (id === 'inProgressTasks' ? 'in_progress' : 'completed'));
1839
- const status = category === 'backlog' ? 'backlog' : (category === 'todo' ? 'pending,blocked,canceled' : (category === 'in_progress' ? 'in_progress' : 'completed'));
1840
- loadTaskCategory(status);
1841
- }
1842
- };
1843
- });
1844
- }
1845
-
1846
- function renderTaskCards(containerId, tasks, clear = false) {
1847
- const container = document.getElementById(containerId);
1848
- if (!container) return;
1849
-
1850
- if (clear && (!tasks || tasks.length === 0)) {
1851
- 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>';
1852
- return;
1853
- }
1854
-
1855
- const html = tasks.map(t => `
1856
- <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">
1857
- <div class="flex items-center justify-between mb-2">
1858
- <div class="flex items-center gap-2">
1859
- <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>
1860
- <span class="text-[10px] font-bold uppercase tracking-wider text-gray-400">${t.phase}</span>
1861
- ${t.status === 'blocked' ? '<span class="px-1 py-0.5 rounded bg-red-500 text-white text-[8px] font-bold uppercase">Blocked</span>' : ''}
1862
- ${t.status === 'canceled' ? '<span class="px-1 py-0.5 rounded bg-slate-500 text-white text-[8px] font-bold uppercase">Canceled</span>' : ''}
1863
- </div>
1864
- <div class="flex items-center gap-1">
1865
- ${t.priority >= 4 ? '<span class="w-2 h-2 rounded-full bg-red-500 animate-pulse"></span>' : ''}
1866
- <span class="text-[10px] font-bold ${getPriorityColor(t.priority)}">P${t.priority}</span>
1867
- </div>
1868
- </div>
1869
- <h4 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-1">${escapeHtml(t.title)}</h4>
1870
- <p class="text-xs text-gray-500 dark:text-gray-400 line-clamp-2 mb-2">${escapeHtml(t.description || '')}</p>
1871
-
1872
- ${t.doc_path ? `
1873
- <div class="mb-3" onclick="event.stopPropagation()">
1874
- <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">
1875
- <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>
1876
- <span class="truncate max-w-[150px]">${escapeHtml(t.doc_path)}</span>
1877
- </a>
1878
- </div>
1879
- ` : ''}
1880
-
1881
- <div class="flex items-center gap-2 mb-3">
1882
- <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">
1883
- <span class="text-[9px] font-bold text-sky-600 dark:text-sky-400">${escapeHtml(t.agent || 'unknown')}</span>
1884
- <span class="text-[8px] text-sky-400 dark:text-sky-600">|</span>
1885
- <span class="text-[9px] font-medium text-sky-500 dark:text-sky-500">${escapeHtml(t.role || 'unknown')}</span>
1886
- </div>
1887
- </div>
1888
-
1889
- ${t.depends_on ? `
1890
- <div class="mt-2 pt-2 border-t border-gray-50 dark:border-gray-600 flex items-center gap-1.5">
1891
- <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>
1892
- <span class="text-[10px] font-medium text-amber-600 dark:text-amber-400">Depends on: ${t.depends_on_code || t.depends_on.substring(0, 8)}</span>
1893
- </div>
1894
- ` : ''}
1895
- <div class="mt-3 flex items-center justify-between">
1896
- <span class="text-[10px] font-mono text-gray-400">${t.id.substring(0, 8)}</span>
1897
- <span class="text-[10px] text-gray-400">${t.status === 'completed' && t.finished_at ? 'Done ' + formatDate(t.finished_at) : formatDate(t.created_at)}</span>
1898
- </div>
1899
- </div>
1900
- `).join('');
1901
-
1902
- if (clear) {
1903
- container.innerHTML = html;
1904
- } else {
1905
- container.insertAdjacentHTML('beforeend', html);
1906
- }
1907
- }
1908
-
1909
- let taskTimeStats = null;
1910
-
1911
- async function updateTimeStats(period) {
1912
- const btns = ['daily', 'weekly', 'monthly', 'overall'];
1913
- btns.forEach(p => {
1914
- const btn = document.getElementById(`${p}StatsBtn`);
1915
- if (btn) {
1916
- if (p === period) {
1917
- btn.classList.add('bg-white', 'dark:bg-slate-800', 'shadow-sm', 'text-sky-600', 'dark:text-sky-400');
1918
- btn.classList.remove('text-slate-500', 'hover:text-slate-700', 'dark:hover:text-slate-300');
1919
- } else {
1920
- btn.classList.remove('bg-white', 'dark:bg-slate-800', 'shadow-sm', 'text-sky-600', 'dark:text-sky-400');
1921
- btn.classList.add('text-slate-500', 'hover:text-slate-700', 'dark:hover:text-slate-300');
1922
- }
1923
- }
1924
- });
1925
-
1926
- const labels = {
1927
- daily: 'Today',
1928
- weekly: 'This Week',
1929
- monthly: 'This Month',
1930
- overall: 'Overall'
1931
- };
1932
-
1933
- for (let i = 1; i <= 4; i++) {
1934
- const el = document.getElementById(`statsPeriodLabel${i}`);
1935
- if (el) {
1936
- const currentText = el.textContent;
1937
- const type = currentText.split(' ').slice(1).join(' ');
1938
- el.textContent = `${labels[period]} ${type}`;
1939
- }
1940
- }
1941
-
1942
- try {
1943
- if (!taskTimeStats) {
1944
- const resp = await fetch(`/api/tasks/stats/time?repo=${encodeURIComponent(currentRepo)}`);
1945
- taskTimeStats = await resp.json();
1946
- }
1947
-
1948
- const stats = taskTimeStats[period];
1949
- document.getElementById('todayCompletedTasksCount').textContent = stats.completed;
1950
- document.getElementById('todayAddedTasksCount').textContent = stats.added;
1951
- document.getElementById('todayTokensTasksCount').textContent = stats.tokens.toLocaleString();
1952
-
1953
- const mins = Math.round(stats.avgDuration / 60);
1954
- document.getElementById('todayAvgTimeTasksCount').textContent = `${mins}m`;
1955
- } catch (err) {
1956
- console.error('Failed to load time stats:', err);
1957
- }
1958
- }
1959
-
1960
- async function exportAllData() {
1961
- try {
1962
- const resp = await fetch(`/api/export?repo=${encodeURIComponent(currentRepo)}`);
1963
- const data = await resp.json();
1964
-
1965
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
1966
- const url = URL.createObjectURL(blob);
1967
- const a = document.createElement('a');
1968
- a.href = url;
1969
- a.download = `mcp-export-${currentRepo.replace(/\//g, '-')}-${new Date().toISOString().split('T')[0]}.json`;
1970
- document.body.appendChild(a);
1971
- a.click();
1972
- document.body.removeChild(a);
1973
- URL.revokeObjectURL(url);
1974
-
1975
- showToast('Export successful', 'success');
1976
- } catch (err) {
1977
- showToast('Export failed: ' + err.message, 'error');
1978
- }
1979
- }
1980
-
1981
- async function updateTaskStatus(id, newStatus, comment = '') {
1982
- try {
1983
- const resp = await fetch(`/api/tasks/${id}`, {
1984
- method: 'PUT',
1985
- headers: { 'Content-Type': 'application/json' },
1986
- body: JSON.stringify({ status: newStatus, comment })
1987
- });
1988
-
1989
- if (resp.ok) {
1990
- showToast(`Task moved to ${newStatus}`, 'success');
1991
- loadTasks();
1992
- showTaskDetail(id); // Refresh detail view
1993
- } else {
1994
- const err = await resp.json();
1995
- throw new Error(err.error || 'Update failed');
1996
- }
1997
- } catch (err) {
1998
- showToast('Failed to update task: ' + err.message, 'error');
1999
- }
2000
- }
2001
-
2002
- let isEditingTask = false;
2003
- async function toggleTaskEdit(id) {
2004
- if (isEditingTask) {
2005
- await saveTaskEdits(id);
2006
- } else {
2007
- enterTaskEditMode();
2008
- }
2009
- }
2010
-
2011
- function enterTaskEditMode() {
2012
- isEditingTask = true;
2013
- const titleEl = document.getElementById('taskDetailTitle');
2014
- const descEl = document.getElementById('taskDetailDesc');
2015
- const editBtn = document.getElementById('taskEditBtn');
2016
-
2017
- if (titleEl) {
2018
- const titleText = titleEl.textContent;
2019
- titleEl.innerHTML = `<input type="text" id="editTaskTitle" class="w-full p-2 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg text-lg font-bold" value="${escapeHtml(titleText)}">`;
2020
- }
2021
-
2022
- if (descEl) {
2023
- const descText = descEl.dataset.raw || '';
2024
- descEl.innerHTML = `<textarea id="editTaskDesc" class="w-full p-3 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg text-sm min-h-[150px] font-mono">${escapeHtml(descText)}</textarea>`;
2025
- }
2026
-
2027
- if (editBtn) {
2028
- editBtn.innerHTML = `<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="M5 13l4 4L19 7"></path></svg> Save`;
2029
- editBtn.classList.add('bg-emerald-500', 'text-white');
2030
- editBtn.classList.remove('bg-white', 'dark:bg-slate-800');
2031
- }
2032
- }
2033
-
2034
- async function saveTaskEdits(id) {
2035
- const newTitle = document.getElementById('editTaskTitle').value;
2036
- const newDesc = document.getElementById('editTaskDesc').value;
2037
-
2038
- try {
2039
- const resp = await fetch(`/api/tasks/${id}`, {
2040
- method: 'PUT',
2041
- headers: { 'Content-Type': 'application/json' },
2042
- body: JSON.stringify({ title: newTitle, description: newDesc })
2043
- });
2044
-
2045
- if (resp.ok) {
2046
- showToast('Task updated', 'success');
2047
- isEditingTask = false;
2048
- loadTasks();
2049
- showTaskDetail(id);
2050
- } else {
2051
- const err = await resp.json();
2052
- throw new Error(err.error || 'Update failed');
2053
- }
2054
- } catch (err) {
2055
- showToast('Failed to save changes: ' + err.message, 'error');
2056
- }
2057
- }
2058
-
2059
- async function addTaskComment(taskId) {
2060
- const commentInput = document.getElementById('newTaskComment');
2061
- const comment = commentInput.value.trim();
2062
- if (!comment) return;
2063
-
2064
- try {
2065
- const resp = await fetch(`/api/tasks/${taskId}`, {
2066
- method: 'PUT',
2067
- headers: { 'Content-Type': 'application/json' },
2068
- body: JSON.stringify({ comment })
2069
- });
2070
-
2071
- if (resp.ok) {
2072
- commentInput.value = '';
2073
- showTaskDetail(taskId);
2074
- } else {
2075
- const err = await resp.json();
2076
- throw new Error(err.error || 'Failed to add comment');
2077
- }
2078
- } catch (err) {
2079
- showToast(err.message, 'error');
2080
- }
2081
- }
2082
-
2083
- async function editTaskComment(commentId, taskId) {
2084
- const commentEl = document.getElementById(`comment-body-${commentId}`);
2085
- const currentText = commentEl.dataset.raw || '';
2086
-
2087
- const newText = prompt('Edit comment:', currentText);
2088
- if (newText === null || newText === currentText) return;
2089
-
2090
- try {
2091
- const resp = await fetch(`/api/task-comments/${commentId}`, {
2092
- method: 'PUT',
2093
- headers: { 'Content-Type': 'application/json' },
2094
- body: JSON.stringify({ comment: newText })
2095
- });
2096
-
2097
- if (resp.ok) {
2098
- showTaskDetail(taskId);
2099
- } else {
2100
- throw new Error('Failed to update comment');
2101
- }
2102
- } catch (err) {
2103
- showToast(err.message, 'error');
2104
- }
2105
- }
2106
-
2107
- async function deleteTaskComment(commentId, taskId) {
2108
- if (!confirm('Are you sure you want to delete this comment?')) return;
2109
-
2110
- try {
2111
- const resp = await fetch(`/api/task-comments/${commentId}`, {
2112
- method: 'DELETE'
2113
- });
2114
-
2115
- if (resp.ok) {
2116
- showTaskDetail(taskId);
2117
- } else {
2118
- throw new Error('Failed to delete comment');
2119
- }
2120
- } catch (err) {
2121
- showToast(err.message, 'error');
2122
- }
2123
- }
2124
-
2125
- async function showTaskDetail(id) {
2126
- const drawer = document.getElementById('memoryDrawer');
2127
- const title = document.getElementById('drawerTitle');
2128
- const body = document.getElementById('drawerBody');
2129
-
2130
- title.textContent = 'Loading Task...';
2131
- body.innerHTML = `
2132
- <div class="space-y-4">
2133
- <div class="skeleton h-20 w-full"></div>
2134
- <div class="grid gap-4 md:grid-cols-2">
2135
- <div class="skeleton h-36 w-full"></div>
2136
- <div class="skeleton h-36 w-full"></div>
2137
- </div>
2138
- <div class="skeleton h-64 w-full"></div>
2139
- </div>
2140
- `;
2141
-
2142
- drawer.classList.remove('hidden');
2143
- document.body.classList.add('drawer-open');
2144
-
2145
- setTimeout(() => {
2146
- const aside = document.getElementById('drawerAside');
2147
- if (aside) {
2148
- aside.classList.remove('translate-x-full');
2149
- aside.classList.add('translate-x-0');
2150
- }
2151
- }, 10);
2152
-
2153
- try {
2154
- const response = await fetch(`/api/tasks/${id}?repo=${encodeURIComponent(currentRepo)}`);
2155
- const task = await response.json();
2156
- if (!response.ok) throw new Error(task.error || 'Failed to load task');
2157
-
2158
- title.textContent = `Task: ${task.task_code}`;
2159
- isEditingTask = false;
2160
-
2161
- const statusButtons = {
2162
- backlog: ['pending', 'blocked'],
2163
- pending: ['in_progress', 'backlog', 'blocked'],
2164
- in_progress: ['pending', 'backlog', 'blocked'],
2165
- blocked: ['pending', 'backlog'],
2166
- completed: ['pending']
2167
- };
2168
-
2169
- const currentButtons = statusButtons[task.status] || [];
2170
- const actionButtonsHtml = currentButtons.map(s => `
2171
- <button onclick="updateTaskStatus('${task.id}', '${s}')" class="px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider transition-all border border-slate-200 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-800">
2172
- Set to ${formatTaskStatusLabel(s)}
2173
- </button>
2174
- `).join('');
2175
-
2176
- body.innerHTML = `
2177
- <div class="space-y-6">
2178
- <div class="flex items-center justify-between gap-4">
2179
- <div class="flex-1 p-5 bg-slate-50 dark:bg-slate-900/50 rounded-2xl border border-slate-100 dark:border-slate-800 shadow-sm">
2180
- <div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-2">Title</div>
2181
- <h3 id="taskDetailTitle" class="text-xl font-bold text-slate-900 dark:text-white leading-tight">${escapeHtml(task.title)}</h3>
2182
- </div>
2183
- <button id="taskEditBtn" onclick="toggleTaskEdit('${task.id}')" class="p-3 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-2xl shadow-sm hover:shadow-md transition-all group">
2184
- <svg class="w-5 h-5 text-slate-400 group-hover:text-sky-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
2185
- </button>
2186
- </div>
2187
-
2188
- <div class="flex flex-wrap gap-2 px-1">
2189
- ${actionButtonsHtml}
2190
- </div>
2191
-
2192
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
2193
- <div class="p-4 bg-white dark:bg-slate-800/40 rounded-xl border border-slate-100 dark:border-slate-800">
2194
- <div class="text-[10px] font-bold text-slate-400 uppercase mb-2">Status & Priority</div>
2195
- <div class="flex flex-wrap gap-2">
2196
- <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>
2197
- <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>
2198
- <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>
2199
- </div>
2200
- ${task.depends_on ? `
2201
- <div class="mt-3 pt-3 border-t border-slate-100 dark:border-slate-700/50 flex items-center gap-2">
2202
- <svg class="w-3.5 h-3.5 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>
2203
- <div class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Depends on:</div>
2204
- <span class="text-xs font-mono font-bold text-amber-600 dark:text-amber-400 bg-amber-500/10 px-1.5 py-0.5 rounded border border-amber-500/20">${task.depends_on_code || task.depends_on.substring(0, 8)}</span>
2205
- </div>
2206
- ` : ''}
2207
- </div>
2208
- <div class="p-4 bg-white dark:bg-slate-800/40 rounded-xl border border-slate-100 dark:border-slate-800">
2209
- <div class="text-[10px] font-bold text-slate-400 uppercase mb-2">Owner</div>
2210
- <div class="flex items-center gap-2">
2211
- <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">
2212
- ${getRepoInitials(task.agent || 'UK')}
2213
- </div>
2214
- <div>
2215
- <div class="text-xs font-bold">${escapeHtml(task.agent || 'unknown')}</div>
2216
- <div class="text-[10px] text-slate-500 dark:text-slate-400">${escapeHtml(task.role || 'unknown')}</div>
2217
- </div>
2218
- </div>
2219
- </div>
2220
- </div>
2221
-
2222
- <div class="p-5 bg-white dark:bg-slate-800/40 rounded-2xl border border-slate-100 dark:border-slate-800">
2223
- <div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-3">Description</div>
2224
- <div id="taskDetailDesc" data-raw="${escapeHtml(task.description || '')}" class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed markdown-body">
2225
- ${task.description ? renderMarkdown(task.description) : '<span class="italic opacity-50">No description provided</span>'}
2226
- </div>
2227
- </div>
2228
-
2229
- ${renderTaskComments(task.comments, task.id)}
2230
-
2231
- ${task.doc_path ? `
2232
- <div class="p-4 bg-sky-500/5 dark:bg-sky-500/10 rounded-xl border border-sky-500/20">
2233
- <div class="text-[10px] font-bold text-sky-600/60 dark:text-sky-400/60 uppercase mb-2">Documentation</div>
2234
- <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()">
2235
- <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>
2236
- <span class="text-sm font-medium truncate">${escapeHtml(task.doc_path)}</span>
2237
- </a>
2238
- </div>
2239
- ` : ''}
2240
-
2241
- <div class="pt-4 border-t border-slate-100 dark:border-slate-800 grid grid-cols-2 gap-4">
2242
- <div>
2243
- <div class="text-[9px] font-bold text-slate-400 uppercase">Created</div>
2244
- <div class="text-[11px] text-slate-500">${new Date(task.created_at).toLocaleString()}</div>
2245
- </div>
2246
- <div>
2247
- <div class="text-[9px] font-bold text-slate-400 uppercase">Updated</div>
2248
- <div class="text-[11px] text-slate-500">${new Date(task.updated_at).toLocaleString()}</div>
2249
- </div>
2250
- </div>
2251
- </div>
2252
- `;
2253
- } catch (err) {
2254
- showToast('Failed to load task: ' + err.message, 'error');
2255
- closeDrawer();
2256
- }
2257
- }
2258
-
2259
- function getPriorityColor(p) {
2260
- if (p >= 5) return 'text-red-600 dark:text-red-400';
2261
- if (p >= 4) return 'text-orange-600 dark:text-orange-400';
2262
- if (p >= 3) return 'text-amber-600 dark:text-amber-400';
2263
- return 'text-gray-500 dark:text-gray-400';
2264
- }
2265
-
2266
- function formatTaskStatusLabel(status) {
2267
- return String(status || '')
2268
- .replace(/_/g, ' ')
2269
- .replace(/\b\w/g, (char) => char.toUpperCase());
2270
- }
2271
-
2272
- function renderTaskComments(comments, taskId) {
2273
- const commentsList = (!comments || comments.length === 0) ? `
2274
- <div class="text-sm italic text-slate-400 py-4">No historical comments yet</div>
2275
- ` : `
2276
- <div class="space-y-4 mt-4">
2277
- ${comments.map((item) => `
2278
- <div class="relative pl-5">
2279
- <div class="absolute left-0 top-1.5 h-full w-px bg-slate-200 dark:bg-slate-700"></div>
2280
- <div class="absolute left-[-4px] top-1.5 w-2.5 h-2.5 rounded-full bg-sky-500 shadow-[0_0_0_4px_rgba(14,165,233,0.12)]"></div>
2281
- <div class="rounded-xl border border-slate-100 dark:border-slate-700 bg-slate-50/80 dark:bg-slate-900/40 p-4 group/comment">
2282
- <div class="flex flex-wrap items-center gap-2 mb-2">
2283
- <span class="text-xs font-bold text-slate-800 dark:text-slate-100">${escapeHtml(item.agent || 'unknown')}</span>
2284
- <span class="text-[10px] text-slate-400">•</span>
2285
- <span class="text-[11px] text-slate-500 dark:text-slate-400">${escapeHtml(item.model || 'unknown')}</span>
2286
- <span class="ml-auto text-[10px] text-slate-400">${new Date(item.created_at).toLocaleString()}</span>
2287
-
2288
- <div class="hidden group-hover/comment:flex items-center gap-1 ml-2">
2289
- <button onclick="editTaskComment('${item.id}', '${taskId}')" class="p-1 text-slate-400 hover:text-sky-500 transition-colors" title="Edit comment">
2290
- <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="M11 5H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
2291
- </button>
2292
- <button onclick="deleteTaskComment('${item.id}', '${taskId}')" class="p-1 text-slate-400 hover:text-rose-500 transition-colors" title="Delete comment">
2293
- <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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
2294
- </button>
2295
- </div>
2296
- </div>
2297
- ${(item.previous_status || item.next_status) ? `
2298
- <div class="flex flex-wrap items-center gap-2 mb-3 text-[10px] font-bold uppercase tracking-wide">
2299
- <span class="px-2 py-1 rounded-lg bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-300">${escapeHtml(formatTaskStatusLabel(item.previous_status || 'note'))}</span>
2300
- <span class="text-slate-400">→</span>
2301
- <span class="px-2 py-1 rounded-lg bg-sky-500/10 text-sky-600 dark:text-sky-400 border border-sky-500/20">${escapeHtml(formatTaskStatusLabel(item.next_status || 'note'))}</span>
2302
- </div>
2303
- ` : ''}
2304
- <div id="comment-body-${item.id}" data-raw="${escapeHtml(item.comment)}" class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed markdown-body">
2305
- ${renderMarkdown(item.comment)}
2306
- </div>
2307
- </div>
2308
- </div>
2309
- `).join('')}
2310
- </div>
2311
- `;
2312
-
2313
- return `
2314
- <div class="p-5 bg-white dark:bg-slate-800/40 rounded-2xl border border-slate-100 dark:border-slate-800">
2315
- <div class="flex items-center justify-between gap-3 mb-4">
2316
- <div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Discussion & History</div>
2317
- <div class="text-[10px] text-slate-400">${comments ? comments.length : 0} item${comments && comments.length === 1 ? '' : 's'}</div>
2318
- </div>
2319
-
2320
- <div class="mb-6">
2321
- <textarea id="newTaskComment" placeholder="Add a comment (Markdown supported)..." class="w-full p-3 bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl text-sm focus:ring-2 focus:ring-sky-500/50 outline-none transition-all min-h-[80px]"></textarea>
2322
- <div class="flex justify-end mt-2">
2323
- <button onclick="addTaskComment('${taskId}')" class="px-4 py-1.5 bg-sky-500 hover:bg-sky-600 text-white text-xs font-bold rounded-lg transition-all shadow-sm shadow-sky-500/20">Post Comment</button>
2324
- </div>
2325
- </div>
2326
-
2327
- <div class="border-t border-slate-100 dark:border-slate-700/50 pt-2">
2328
- ${commentsList}
2329
- </div>
2330
- </div>
2331
- `;
2332
- }
2333
- let currentTab = localStorage.getItem('activeTab') || 'dashboard';
2334
-
2335
- function syncTabIndicatorTheme(indicator) {
2336
- const isDark = document.documentElement.classList.contains('dark');
2337
- indicator.style.background = isDark
2338
- ? 'linear-gradient(135deg, rgba(14,165,233,0.95) 0%, rgba(79,70,229,0.92) 100%)'
2339
- : 'linear-gradient(135deg, #0ea5e9 0%, #2563eb 100%)';
2340
- indicator.style.border = isDark
2341
- ? '1px solid rgba(59, 130, 246, 0.32)'
2342
- : '1px solid rgba(37, 99, 235, 0.18)';
2343
- indicator.style.boxShadow = isDark
2344
- ? '0 12px 28px rgba(14,165,233,0.2), inset 0 1px 0 rgba(255,255,255,0.12)'
2345
- : '0 10px 24px rgba(37,99,235,0.24), inset 0 1px 0 rgba(255,255,255,0.28)';
2346
- }
2347
-
2348
- function syncActiveTabButtonTheme(button, isActive) {
2349
- if (!button) return;
2350
-
2351
- const isDark = document.documentElement.classList.contains('dark');
2352
-
2353
- if (!isActive) {
2354
- button.style.color = '';
2355
- button.style.background = '';
2356
- button.style.border = '';
2357
- button.style.boxShadow = '';
2358
- return;
2359
- }
2360
-
2361
- button.style.color = isDark ? '#ffffff' : '#0f172a';
2362
- button.style.background = isDark
2363
- ? 'linear-gradient(135deg, rgba(14,165,233,0.95) 0%, rgba(79,70,229,0.92) 100%)'
2364
- : 'rgba(255,255,255,0.96)';
2365
- button.style.border = isDark
2366
- ? '1px solid rgba(59, 130, 246, 0.32)'
2367
- : '1px solid rgba(148, 163, 184, 0.22)';
2368
- button.style.boxShadow = isDark
2369
- ? '0 12px 28px rgba(14,165,233,0.2), inset 0 1px 0 rgba(255,255,255,0.12)'
2370
- : '0 10px 24px rgba(148,163,184,0.16), inset 0 -3px 0 rgba(37,99,235,0.9)';
2371
- }
2372
-
2373
- function positionTabIndicator(tab) {
2374
- const indicator = document.getElementById('tabIndicator');
2375
- const rail = document.getElementById('tabRail');
2376
- const targetButton = document.getElementById(`${tab}TabBtn`);
2377
-
2378
- if (!indicator || !rail || !targetButton) return;
2379
- syncTabIndicatorTheme(indicator);
2380
-
2381
- const railRect = rail.getBoundingClientRect();
2382
- const buttonRect = targetButton.getBoundingClientRect();
2383
- const left = buttonRect.left - railRect.left;
2384
-
2385
- indicator.style.left = `${left}px`;
2386
- indicator.style.width = `${buttonRect.width}px`;
2387
- indicator.style.transform = 'none';
2388
- }
2389
-
2390
- function scheduleTabIndicatorPosition(tab = currentTab) {
2391
- requestAnimationFrame(() => {
2392
- requestAnimationFrame(() => {
2393
- positionTabIndicator(tab);
2394
- });
2395
- });
2396
- }
2397
-
2398
- function switchTab(tab) {
2399
- const dashTab = document.getElementById('dashboardTabBtn');
2400
- const memTab = document.getElementById('memoriesTabBtn');
2401
- const taskTab = document.getElementById('tasksTabBtn');
2402
- const refTab = document.getElementById('referenceTabBtn');
2403
-
2404
- const dashContent = document.getElementById('dashboardContent');
2405
- const memContent = document.getElementById('memoriesContent');
2406
- const taskContent = document.getElementById('tasksContent');
2407
- const refContent = document.getElementById('referenceContent');
2408
-
2409
- currentTab = tab;
2410
- localStorage.setItem('activeTab', tab);
2411
-
2412
- const tabs = [dashTab, memTab, taskTab, refTab];
2413
- const contents = [dashContent, memContent, taskContent, refContent]; const targetId = tab + 'TabBtn';
2414
- const targetContentId = tab + 'Content';
2415
-
2416
- scheduleTabIndicatorPosition(tab);
2417
-
2418
- // Update button states
2419
- tabs.forEach(t => {
2420
- if (t) {
2421
- if (t.id === targetId) {
2422
- t.classList.add('tab-active');
2423
- t.classList.add('text-white');
2424
- t.classList.remove('text-gray-500', 'dark:text-gray-400');
2425
- syncActiveTabButtonTheme(t, true);
2426
- } else {
2427
- t.classList.remove('tab-active');
2428
- t.classList.remove('text-white');
2429
- t.classList.add('text-gray-500', 'dark:text-gray-400');
2430
- syncActiveTabButtonTheme(t, false);
2431
- }
2432
- }
2433
- });
2434
-
2435
- // Update content visibility
2436
- contents.forEach(c => {
2437
- if (c) {
2438
- if (c.id === targetContentId) {
2439
- c.classList.remove('hidden');
2440
- } else {
2441
- c.classList.add('hidden');
2442
- }
2443
- }
2444
- });
2445
-
2446
- // Trigger data loads if needed
2447
- if (tab === 'tasks') loadTasks();
2448
- if (tab === 'dashboard') loadStats();
2449
- if (tab === 'memories') loadMemories();
2450
- if (tab === 'reference') loadCapabilities();
2451
- syncStickyOffsets();
2452
- }
2453
-
2454
- window.addEventListener('resize', () => {
2455
- scheduleTabIndicatorPosition(currentTab);
2456
- });
2457
-
2458
- if (typeof ResizeObserver !== 'undefined') {
2459
- const rail = document.getElementById('tabRail');
2460
- if (rail) {
2461
- new ResizeObserver(() => {
2462
- scheduleTabIndicatorPosition(currentTab);
2463
- }).observe(rail);
2464
- }
2465
- }
2466
-
2467
- window.loadTasks = loadTasks;
2468
- window.switchTab = switchTab;
2469
- window.charts = charts;
2470
-
2471
- const safeAddEventListener = (id, event, handler) => {
2472
- const el = document.getElementById(id);
2473
- if (el) el.addEventListener(event, handler);
2474
- };
2475
-
2476
- safeAddEventListener('repoSearchInput', 'input', () => {
2477
- renderRepoSidebar();
2478
- });
2479
-
2480
- safeAddEventListener('repoSearchInput', 'keydown', (e) => {
2481
- if (e.key === 'Enter') {
2482
- const firstMatch = availableRepos.find((item) => item.repo.toLowerCase().includes(e.target.value.trim().toLowerCase()));
2483
- if (firstMatch) {
2484
- setCurrentRepo(firstMatch.repo);
2485
- }
2486
- }
2487
- });
2488
-
2489
- safeAddEventListener('repoSearchInputMobile', 'input', () => {
2490
- renderRepoSidebar();
2491
- });
2492
-
2493
- safeAddEventListener('repoSearchInputMobile', 'keydown', (e) => {
2494
- if (e.key === 'Enter') {
2495
- const firstMatch = availableRepos.find((item) => item.repo.toLowerCase().includes(e.target.value.trim().toLowerCase()));
2496
- if (firstMatch) {
2497
- setCurrentRepo(firstMatch.repo);
2498
- }
2499
- }
2500
- });
2501
-
2502
- safeAddEventListener('repoNavToggle', 'click', () => {
2503
- openRepoSidebarDrawer();
2504
- });
2505
-
2506
- safeAddEventListener('repoSidebarCollapseToggle', 'click', () => {
2507
- toggleRepoSidebarCollapse();
2508
- });
2509
-
2510
- safeAddEventListener('repoCollapsedSummaryButton', 'click', () => {
2511
- if (isRepoSidebarCollapsed) {
2512
- toggleRepoSidebarCollapse();
2513
- }
2514
- });
2515
-
2516
- safeAddEventListener('taskSearchInput', 'input', () => {
2517
- if (window.taskSearchDebounce) clearTimeout(window.taskSearchDebounce);
2518
- window.taskSearchDebounce = setTimeout(() => {
2519
- loadTasks();
2520
- }, 300);
2521
- });
2522
-
2523
- window.addEventListener('resize', syncStickyOffsets);
2524
-
2525
- let currentCapabilities = { tools: [], resources: [], prompts: [] };
2526
-
2527
- async function loadCapabilities() {
2528
- const toolsList = document.getElementById('toolsList');
2529
- const resourcesList = document.getElementById('resourcesList');
2530
- const promptsList = document.getElementById('promptsList');
2531
-
2532
- if (!toolsList) return;
2533
-
2534
- toolsList.innerHTML = '<div class="text-center py-4 text-gray-400 text-xs">Loading capabilities...</div>';
2535
-
2536
- try {
2537
- const response = await fetch('/api/capabilities');
2538
- const data = await response.json();
2539
-
2540
- currentCapabilities.tools = Array.isArray(data.tools) ? data.tools : [];
2541
- currentCapabilities.resources = Array.isArray(data.resources) ? data.resources : [];
2542
- currentCapabilities.prompts = Array.isArray(data.prompts) ? data.prompts : [];
2543
-
2544
- toolsList.innerHTML = currentCapabilities.tools.map(t => `
2545
- <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">
2546
- <div class="font-bold text-xs text-sky-600 dark:text-sky-400 mb-1 group-hover:text-sky-500">${escapeHtml(t.name)}</div>
2547
- <p class="text-[10px] text-gray-500 dark:text-gray-400 line-clamp-2">${escapeHtml(t.description)}</p>
2548
- </div>
2549
- `).join('') || '<div class="text-center py-4 text-gray-400 text-xs">No tools available</div>';
2550
-
2551
- resourcesList.innerHTML = currentCapabilities.resources.map(r => `
2552
- <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">
2553
- <div class="font-bold text-xs text-indigo-600 dark:text-indigo-400 mb-1 group-hover:text-indigo-500">${escapeHtml(r.name)}</div>
2554
- <p class="text-[10px] text-gray-500 dark:text-gray-400 line-clamp-2">${escapeHtml(r.description || 'No description')}</p>
2555
- <div class="text-[8px] font-mono text-gray-400 mt-1 truncate">${escapeHtml(r.uri)}</div>
2556
- </div>
2557
- `).join('') || '<div class="text-center py-4 text-gray-400 text-xs">No resources available</div>';
2558
-
2559
- promptsList.innerHTML = currentCapabilities.prompts.map(p => `
2560
- <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">
2561
- <div class="font-bold text-xs text-emerald-600 dark:text-emerald-400 mb-1 group-hover:text-emerald-500">${escapeHtml(p.name)}</div>
2562
- <p class="text-[10px] text-gray-500 dark:text-gray-400 line-clamp-2">${escapeHtml(p.description || 'No description')}</p>
2563
- </div>
2564
- `).join('') || '<div class="text-center py-4 text-gray-400 text-xs">No prompts available</div>';
2565
-
2566
- } catch (err) {
2567
- console.error('Failed to load capabilities:', err);
2568
- toolsList.innerHTML = `<div class="text-center py-4 text-red-400 text-xs">Error: ${err.message}</div>`;
2569
- }
2570
- }
2571
-
2572
- async function handleCsvImport(event) {
2573
- const file = event.target.files[0];
2574
- if (!file || !currentRepo) return;
2575
-
2576
- const reader = new FileReader();
2577
- reader.onload = async (e) => {
2578
- const csvData = e.target.result;
2579
- try {
2580
- const response = await fetch('/api/tasks/import-csv', {
2581
- method: 'POST',
2582
- headers: { 'Content-Type': 'application/json' },
2583
- body: JSON.stringify({ repo: currentRepo, csvData })
2584
- });
2585
- const result = await response.json();
2586
- if (result.success) {
2587
- showToast(`Successfully imported ${result.count} tasks`, 'success');
2588
- loadTasks();
2589
- loadStats();
2590
- } else {
2591
- showToast(result.error || 'Failed to import CSV', 'error');
2592
- }
2593
- } catch (err) {
2594
- showToast('Import failed: ' + err.message, 'error');
2595
- }
2596
- };
2597
- reader.readAsText(file);
2598
- event.target.value = '';
2599
- }
2600
-
2601
- function downloadCsvTemplate() {
2602
- const headers = "task_code,phase,title,description,priority,status,agent,role,doc_path";
2603
- const example = "TASK-001,research,Integrate CSV,Add import feature to dashboard,4,pending,Gemini CLI,expert,docs/tasks.md";
2604
- const csv = `${headers}\n${example}`;
2605
-
2606
- const blob = new Blob([csv], { type: 'text/csv' });
2607
- const url = window.URL.createObjectURL(blob);
2608
- const a = document.createElement('a');
2609
- a.setAttribute('hidden', '');
2610
- a.setAttribute('href', url);
2611
- a.setAttribute('download', 'task_template.csv');
2612
- document.body.appendChild(a);
2613
- a.click();
2614
- document.body.removeChild(a);
2615
- }
2616
-
2617
- function showCapabilityDetail(type, name) {
2618
- const item = currentCapabilities[type].find(i => i.name === name);
2619
- if (!item) return;
2620
-
2621
- const drawer = document.getElementById('memoryDrawer');
2622
- const title = document.getElementById('drawerTitle');
2623
- const body = document.getElementById('drawerBody');
2624
-
2625
- title.textContent = `${type.charAt(0).toUpperCase() + type.slice(1)}: ${name}`;
2626
-
2627
- let contentHtml = `
2628
- <div class="space-y-6">
2629
- <div class="p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
2630
- <h4 class="text-xs font-bold text-slate-400 uppercase mb-2">Description</h4>
2631
- <p class="text-sm text-slate-600 dark:text-slate-300">${escapeHtml(item.description || 'No description')}</p>
2632
- </div>
2633
- `;
2634
-
2635
- if (type === 'tools' && item.inputSchema) {
2636
- contentHtml += `
2637
- <div class="p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
2638
- <h4 class="text-xs font-bold text-slate-400 uppercase mb-2">Input Schema</h4>
2639
- <pre class="text-[10px] font-mono text-sky-600 dark:text-sky-400 overflow-x-auto">${JSON.stringify(item.inputSchema, null, 2)}</pre>
2640
- </div>
2641
- `;
2642
- }
2643
-
2644
- if (type === 'resources' && item.uri) {
2645
- contentHtml += `
2646
- <div class="p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
2647
- <h4 class="text-xs font-bold text-slate-400 uppercase mb-2">URI</h4>
2648
- <code class="text-xs font-mono text-indigo-600 dark:text-indigo-400">${escapeHtml(item.uri)}</code>
2649
- </div>
2650
- `;
2651
- }
2652
-
2653
- if (type === 'prompts' && item.arguments) {
2654
- contentHtml += `
2655
- <div class="p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
2656
- <h4 class="text-xs font-bold text-slate-400 uppercase mb-2">Arguments</h4>
2657
- <div class="space-y-3">
2658
- ${item.arguments.map(arg => `
2659
- <div class="border-l-2 border-emerald-500 pl-3 py-1">
2660
- <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>
2661
- <div class="text-[10px] text-gray-500 dark:text-gray-400">${escapeHtml(arg.description || '')}</div>
2662
- </div>
2663
- `).join('')}
2664
- </div>
2665
- </div>
2666
- `;
2667
- }
2668
-
2669
- if (type === 'prompts' && item.messages) {
2670
- contentHtml += `
2671
- <div class="p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
2672
- <h4 class="text-xs font-bold text-slate-400 uppercase mb-2">Instructions</h4>
2673
- <div class="space-y-4">
2674
- ${item.messages.map(msg => {
2675
- const rawContent = typeof msg.content === 'string' ? msg.content : (msg.content?.text || '');
2676
- const renderedMarkdown = window.marked ? window.marked.parse(rawContent) : escapeHtml(rawContent);
2677
- return `
2678
- <div class="space-y-1">
2679
- <div class="text-[10px] font-bold uppercase tracking-wider text-slate-400">${escapeHtml(msg.role)}</div>
2680
- <div class="p-4 bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 rounded-lg text-sm markdown-body">
2681
- ${renderedMarkdown}
2682
- </div>
2683
- </div>
2684
- `;
2685
- }).join('')}
2686
- </div>
2687
- </div>
2688
- `;
2689
- }
2690
-
2691
- contentHtml += `</div>`;
2692
- body.innerHTML = contentHtml;
2693
-
2694
- drawer.classList.remove('hidden');
2695
- document.body.classList.add('drawer-open');
2696
- setTimeout(() => document.getElementById('drawerAside').classList.remove('translate-x-full'), 10);
2697
- }
2698
-
2699
- window.handleCsvImport = handleCsvImport;
2700
- window.downloadCsvTemplate = downloadCsvTemplate;
2701
- window.loadCapabilities = loadCapabilities;
2702
- window.showCapabilityDetail = showCapabilityDetail;
2703
-
2704
- initTheme();
2705
- initRepoSidebarState();
2706
- initPinnedRepos();
2707
- renderRecentActions();
2708
- loadData();
2709
- switchTab(currentTab);
2710
- syncStickyOffsets();
2711
- startCountdown();
2712
- setInterval(checkStatus, 30000);
2713
-
2714
- // Memories Filter & Popover logic
2715
- function toggleFilterPopover() {
2716
- const popover = document.getElementById('filterPopover');
2717
- popover.classList.toggle('hidden');
2718
- updateActiveFilterCount();
2719
- }
2720
-
2721
- function toggleExportPopover() {
2722
- const popover = document.getElementById('exportPopover');
2723
- popover.classList.toggle('hidden');
2724
- }
2725
-
2726
- function updateActiveFilterCount() {
2727
- const type = document.getElementById('typeFilter').value;
2728
- const minImp = document.getElementById('minImportanceFilter').value;
2729
- const maxImp = document.getElementById('maxImportanceFilter').value;
2730
- let count = 0;
2731
- if (type) count++;
2732
- if (minImp) count++;
2733
- if (maxImp) count++;
2734
-
2735
- const badge = document.getElementById('activeFilterCount');
2736
- if (badge) {
2737
- if (count > 0) {
2738
- badge.innerText = count;
2739
- badge.classList.remove('hidden');
2740
- } else {
2741
- badge.classList.add('hidden');
2742
- }
2743
- }
2744
- }
2745
-
2746
- function resetFilters() {
2747
- document.getElementById('typeFilter').value = '';
2748
- document.getElementById('minImportanceFilter').value = '';
2749
- document.getElementById('maxImportanceFilter').value = '';
2750
- updateActiveFilterCount();
2751
- currentPage = 1;
2752
- loadMemories();
2753
- }
2754
-
2755
- document.addEventListener('click', (e) => {
2756
- const filterPopover = document.getElementById('filterPopover');
2757
- const filterBtn = document.getElementById('filterPopoverBtn');
2758
- const exportPopover = document.getElementById('exportPopover');
2759
- const exportBtn = document.getElementById('exportPopoverBtn');
2760
-
2761
- if (filterPopover && !filterPopover.contains(e.target) && !filterBtn.contains(e.target)) {
2762
- filterPopover.classList.add('hidden');
2763
- }
2764
- if (exportPopover && !exportPopover.contains(e.target) && !exportBtn.contains(e.target)) {
2765
- exportPopover.classList.add('hidden');
2766
- }
2767
- });
2768
-
2769
- window.toggleFilterPopover = toggleFilterPopover;
2770
- window.toggleExportPopover = toggleExportPopover;
2771
- window.resetFilters = resetFilters;
2772
-
2773
- // Auto-hide popovers on Escape key
2774
- document.addEventListener('keydown', (e) => {
2775
- if (e.key === 'Escape') {
2776
- document.getElementById('filterPopover')?.classList.add('hidden');
2777
- document.getElementById('exportPopover')?.classList.add('hidden');
2778
- }
2779
- });