synabun 1.3.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 (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,123 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Tooltip System
3
+ // Body-appended, overflow-proof tooltip for data-tooltip attributes
4
+ // ═══════════════════════════════════════════
5
+
6
+ /**
7
+ * Initialize the tooltip system.
8
+ * Creates a body-appended tooltip element and delegates
9
+ * mouseover/mouseout on any [data-tooltip] elements.
10
+ */
11
+ export function initTooltip() {
12
+ const tip = document.createElement('div');
13
+ tip.className = 'ui-tooltip';
14
+ tip.innerHTML = '<span class="ui-tooltip-arrow"></span><span class="ui-tooltip-text"></span>';
15
+ document.body.appendChild(tip);
16
+
17
+ let showTimer = null;
18
+ let hideTimer = null;
19
+ let currentTarget = null;
20
+
21
+ function positionTip(el) {
22
+ const r = el.getBoundingClientRect();
23
+ const tw = tip.offsetWidth;
24
+ const th = tip.offsetHeight;
25
+ const gap = 8;
26
+ const preferred = el.getAttribute('data-tooltip-pos');
27
+
28
+ // Right-side placement (explorer sidebar, whiteboard toolbar)
29
+ const inWbToolbar = !!el.closest('#wb-toolbar');
30
+ if ((preferred === 'right' || inWbToolbar) && r.right + gap + tw < window.innerWidth - 4) {
31
+ tip.className = 'ui-tooltip right';
32
+ tip.style.top = (r.top + r.height / 2 - th / 2) + 'px';
33
+ tip.style.left = (r.right + gap) + 'px';
34
+ const arrow = tip.querySelector('.ui-tooltip-arrow');
35
+ arrow.style.left = '';
36
+ arrow.style.top = (th / 2) + 'px';
37
+ arrow.style.transform = 'translateY(-50%)';
38
+ return;
39
+ }
40
+
41
+ // Elements near the top (title bar) → force below
42
+ const forceBelow = preferred === 'below' || el.closest('#title-bar');
43
+ let top = forceBelow ? r.bottom + gap : r.top - th - gap;
44
+ let placement = forceBelow ? 'below' : 'above';
45
+ if (!forceBelow && top < 4) {
46
+ top = r.bottom + gap;
47
+ placement = 'below';
48
+ }
49
+ let left = r.left + r.width / 2 - tw / 2;
50
+ if (left < 4) left = 4;
51
+ if (left + tw > window.innerWidth - 4) left = window.innerWidth - 4 - tw;
52
+ tip.className = 'ui-tooltip ' + placement;
53
+ tip.style.top = top + 'px';
54
+ tip.style.left = left + 'px';
55
+ const arrowLeft = r.left + r.width / 2 - left;
56
+ const arrow = tip.querySelector('.ui-tooltip-arrow');
57
+ arrow.style.left = arrowLeft + 'px';
58
+ arrow.style.top = '';
59
+ arrow.style.transform = 'translateX(-50%)';
60
+ }
61
+
62
+ function show(el) {
63
+ let text = el.getAttribute('data-tooltip');
64
+ if (!text) return;
65
+ clearTimeout(hideTimer);
66
+ const textEl = tip.querySelector('.ui-tooltip-text');
67
+ const nlIdx = text.indexOf('\n');
68
+ if (nlIdx !== -1) {
69
+ let main = text.slice(0, nlIdx);
70
+ if (main.length > 200) main = main.slice(0, 200) + '\u2026';
71
+ const mainNode = document.createTextNode(main);
72
+ const sub = document.createElement('span');
73
+ sub.className = 'ui-tooltip-sub';
74
+ sub.textContent = text.slice(nlIdx + 1);
75
+ textEl.textContent = '';
76
+ textEl.appendChild(mainNode);
77
+ textEl.appendChild(document.createElement('br'));
78
+ textEl.appendChild(sub);
79
+ } else {
80
+ if (text.length > 200) text = text.slice(0, 200) + '\u2026';
81
+ textEl.textContent = text;
82
+ }
83
+ currentTarget = el;
84
+ tip.style.display = 'block';
85
+ tip.offsetHeight; // force reflow
86
+ positionTip(el);
87
+ tip.classList.add('visible');
88
+ }
89
+
90
+ function hide() {
91
+ clearTimeout(showTimer);
92
+ clearTimeout(hideTimer);
93
+ showTimer = null;
94
+ currentTarget = null;
95
+ tip.classList.remove('visible');
96
+ hideTimer = setTimeout(() => { tip.style.display = 'none'; }, 150);
97
+ }
98
+
99
+ document.addEventListener('mouseover', (e) => {
100
+ const el = e.target.closest('[data-tooltip]');
101
+ if (!el) return;
102
+ if (el === currentTarget) { clearTimeout(hideTimer); return; }
103
+ clearTimeout(showTimer);
104
+ clearTimeout(hideTimer);
105
+ if (currentTarget) {
106
+ hide();
107
+ show(el);
108
+ } else {
109
+ showTimer = setTimeout(() => show(el), 20);
110
+ }
111
+ });
112
+
113
+ document.addEventListener('mouseout', (e) => {
114
+ const el = e.target.closest('[data-tooltip]');
115
+ if (!el) return;
116
+ const related = e.relatedTarget;
117
+ if (related && el.contains(related)) return;
118
+ hide();
119
+ });
120
+
121
+ document.addEventListener('scroll', hide, true);
122
+ document.addEventListener('pointerdown', hide, true);
123
+ }
@@ -0,0 +1,439 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Trash Panel
3
+ // Floating window for viewing, restoring, and purging trashed memories.
4
+ // Styled after the Skills Studio container.
5
+ // ═══════════════════════════════════════════
6
+
7
+ import { emit, on } from './state.js';
8
+ import { fetchTrash, restoreFromTrash, purgeTrash, deleteMemoryPermanent } from './api.js';
9
+ import { formatTrashAge, formatMemoryContent } from './utils.js';
10
+ import { storage } from './storage.js';
11
+ import { isGuest, hasPermission, showGuestToast } from './ui-sync.js';
12
+
13
+ const $ = (id) => document.getElementById(id);
14
+
15
+ // HTML-escape helper
16
+ function esc(s) { return (s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
17
+
18
+ // ─── Module state ────────────────────────
19
+
20
+ let trashPanelOpen = false;
21
+ let trashItems = [];
22
+ let trashSelectedId = null;
23
+ let trashSearchTerm = '';
24
+ let trashFilterCat = 'all'; // 'all' or a specific category slug
25
+ let trashSortBy = 'newest'; // 'newest' | 'oldest' | 'category'
26
+
27
+ // ─── Badge ───────────────────────────────
28
+
29
+ function updateTrashBadge() {
30
+ const badge = $('titlebar-trash-count');
31
+ if (badge) badge.textContent = trashItems.length > 0 ? trashItems.length : '';
32
+ }
33
+
34
+ // ─── Fetch ───────────────────────────────
35
+
36
+ async function fetchTrashItems() {
37
+ try {
38
+ const data = await fetchTrash();
39
+ trashItems = data.items;
40
+ updateTrashBadge();
41
+ if (trashPanelOpen) renderTrashList();
42
+ } catch (err) {
43
+ console.error('fetchTrash error:', err);
44
+ }
45
+ }
46
+
47
+ // ─── Helpers ─────────────────────────────
48
+
49
+ /** Get unique categories from trash items, sorted alphabetically */
50
+ function getTrashCategories() {
51
+ const cats = new Set();
52
+ for (const item of trashItems) {
53
+ cats.add(item.payload.category || 'uncategorized');
54
+ }
55
+ return [...cats].sort();
56
+ }
57
+
58
+ /** Apply search + category filter + sort */
59
+ function getFilteredItems() {
60
+ let list = trashItems;
61
+
62
+ // Category filter
63
+ if (trashFilterCat !== 'all') {
64
+ list = list.filter(i => (i.payload.category || 'uncategorized') === trashFilterCat);
65
+ }
66
+
67
+ // Search filter
68
+ if (trashSearchTerm) {
69
+ list = list.filter(item => {
70
+ const p = item.payload;
71
+ const haystack = [
72
+ p.content, p.category, p.subcategory, p.project, ...(p.tags || []),
73
+ ].filter(Boolean).join(' ').toLowerCase();
74
+ return haystack.includes(trashSearchTerm);
75
+ });
76
+ }
77
+
78
+ // Sort
79
+ if (trashSortBy === 'oldest') {
80
+ list = [...list].sort((a, b) => (a.payload.trashed_at || '').localeCompare(b.payload.trashed_at || ''));
81
+ } else if (trashSortBy === 'category') {
82
+ list = [...list].sort((a, b) => (a.payload.category || '').localeCompare(b.payload.category || ''));
83
+ } else {
84
+ // newest first (default)
85
+ list = [...list].sort((a, b) => (b.payload.trashed_at || '').localeCompare(a.payload.trashed_at || ''));
86
+ }
87
+
88
+ return list;
89
+ }
90
+
91
+ // ─── Open / Close ────────────────────────
92
+
93
+ function openTrashPanel() {
94
+ if (isGuest() && !hasPermission('memories')) {
95
+ showGuestToast('Trash access is disabled by the host');
96
+ return;
97
+ }
98
+ const existing = $('trash-window');
99
+ if (existing) { existing.style.zIndex = '50101'; return; }
100
+
101
+ trashPanelOpen = true;
102
+ trashSelectedId = null;
103
+ trashSearchTerm = '';
104
+ trashFilterCat = 'all';
105
+
106
+ const win = document.createElement('div');
107
+ win.className = 'tw glass resizable';
108
+ win.id = 'trash-window';
109
+
110
+ const saved = JSON.parse(storage.getItem('neural-panel-trash-window') || 'null');
111
+ if (saved) {
112
+ if (saved.left && saved.left !== 'auto') win.style.left = saved.left;
113
+ if (saved.top) win.style.top = Math.max(48, parseInt(saved.top)) + 'px';
114
+ if (saved.width) win.style.width = saved.width;
115
+ if (saved.height) win.style.height = saved.height;
116
+ } else {
117
+ win.style.left = Math.max(20, (window.innerWidth - 640) / 2) + 'px';
118
+ win.style.top = Math.max(48, (window.innerHeight - 480) / 2) + 'px';
119
+ }
120
+
121
+ win.innerHTML = `
122
+ <div class="resize-handle resize-handle-t" data-resize="t"></div>
123
+ <div class="resize-handle resize-handle-b" data-resize="b"></div>
124
+ <div class="resize-handle resize-handle-l" data-resize="l"></div>
125
+ <div class="resize-handle resize-handle-r" data-resize="r"></div>
126
+ <div class="resize-handle resize-handle-tl" data-resize="tl"></div>
127
+ <div class="resize-handle resize-handle-tr" data-resize="tr"></div>
128
+ <div class="resize-handle resize-handle-bl" data-resize="bl"></div>
129
+ <div class="resize-handle resize-handle-br" data-resize="br"></div>
130
+
131
+ <!-- Header -->
132
+ <div class="settings-panel-header drag-handle" data-drag="trash-window">
133
+ <h3>Trash</h3>
134
+ <span class="tw-count" id="tw-h-count">0</span>
135
+ <div class="tw-header-actions">
136
+ <button class="tw-header-btn tw-purge-btn" id="tw-purge" data-tooltip="Permanently delete all">Purge All</button>
137
+ </div>
138
+ <button class="settings-panel-close" id="tw-close" data-tooltip="Close">&times;</button>
139
+ </div>
140
+
141
+ <!-- Body: sidebar + detail -->
142
+ <div class="tw-body">
143
+ <aside class="tw-sidebar">
144
+ <div class="tw-search-wrap">
145
+ <input type="text" class="tw-search" id="tw-search" placeholder="Filter trash…" autocomplete="off" spellcheck="false">
146
+ </div>
147
+ <div class="tw-filters" id="tw-filters"></div>
148
+ <div class="tw-sort" id="tw-sort-wrap">
149
+ <select class="tw-sort-select" id="tw-sort">
150
+ <option value="newest">Newest first</option>
151
+ <option value="oldest">Oldest first</option>
152
+ <option value="category">By category</option>
153
+ </select>
154
+ </div>
155
+ <div class="tw-list" id="tw-list"></div>
156
+ </aside>
157
+ <main class="tw-main" id="tw-detail">
158
+ <div class="tw-empty">Select a memory to preview</div>
159
+ </main>
160
+ </div>
161
+ `;
162
+
163
+ document.body.appendChild(win);
164
+
165
+ // Wire close
166
+ win.querySelector('#tw-close').addEventListener('click', closeTrashPanel);
167
+
168
+ // Wire purge
169
+ const purgeBtn = win.querySelector('#tw-purge');
170
+ purgeBtn.addEventListener('click', async () => {
171
+ if (!trashItems.length) return;
172
+ if (!confirm(`Permanently delete all ${trashItems.length} trashed memor${trashItems.length === 1 ? 'y' : 'ies'}? This cannot be undone.`)) return;
173
+ purgeBtn.textContent = 'Purging…';
174
+ purgeBtn.disabled = true;
175
+ try {
176
+ await purgeTrash();
177
+ trashItems = [];
178
+ trashSelectedId = null;
179
+ updateTrashBadge();
180
+ renderTrashList();
181
+ renderTrashDetail();
182
+ emit('trash:updated');
183
+ } catch (err) { console.error('Purge error:', err); }
184
+ purgeBtn.textContent = 'Purge All';
185
+ purgeBtn.disabled = false;
186
+ });
187
+
188
+ // Wire search
189
+ const searchInput = win.querySelector('#tw-search');
190
+ searchInput.addEventListener('input', () => {
191
+ trashSearchTerm = searchInput.value.toLowerCase().trim();
192
+ renderTrashList();
193
+ });
194
+
195
+ // Wire sort
196
+ const sortSelect = win.querySelector('#tw-sort');
197
+ sortSelect.value = trashSortBy;
198
+ sortSelect.addEventListener('change', () => {
199
+ trashSortBy = sortSelect.value;
200
+ renderTrashList();
201
+ });
202
+
203
+ // Fetch and render
204
+ fetchTrashItems().then(() => {
205
+ renderTrashFilters();
206
+ renderTrashList();
207
+ });
208
+ }
209
+
210
+ function closeTrashPanel() {
211
+ const win = $('trash-window');
212
+ if (win) win.remove();
213
+ trashPanelOpen = false;
214
+ trashSelectedId = null;
215
+ trashSearchTerm = '';
216
+ trashFilterCat = 'all';
217
+ }
218
+
219
+ // ─── Render: Filters ─────────────────────
220
+
221
+ function renderTrashFilters() {
222
+ const container = $('tw-filters');
223
+ if (!container) return;
224
+
225
+ const cats = getTrashCategories();
226
+ let html = `<button class="tw-filter${trashFilterCat === 'all' ? ' active' : ''}" data-cat="all">All</button>`;
227
+ for (const cat of cats) {
228
+ const active = trashFilterCat === cat ? ' active' : '';
229
+ const count = trashItems.filter(i => (i.payload.category || 'uncategorized') === cat).length;
230
+ html += `<button class="tw-filter${active}" data-cat="${esc(cat)}"><span class="tw-filter-dot"></span>${esc(cat)}<span class="tw-filter-count">${count}</span></button>`;
231
+ }
232
+ container.innerHTML = html;
233
+
234
+ // Wire clicks
235
+ container.querySelectorAll('.tw-filter').forEach(btn => {
236
+ btn.addEventListener('click', () => {
237
+ trashFilterCat = btn.dataset.cat;
238
+ container.querySelectorAll('.tw-filter').forEach(b => b.classList.toggle('active', b === btn));
239
+ renderTrashList();
240
+ });
241
+ });
242
+ }
243
+
244
+ // ─── Render: List ────────────────────────
245
+
246
+ function renderTrashList() {
247
+ const win = $('trash-window');
248
+ if (!win) return;
249
+
250
+ const listEl = win.querySelector('#tw-list');
251
+ const countEl = win.querySelector('#tw-h-count');
252
+ const purgeBtn = win.querySelector('#tw-purge');
253
+
254
+ // Update header count pill
255
+ countEl.textContent = trashItems.length;
256
+ purgeBtn.style.display = trashItems.length ? '' : 'none';
257
+
258
+ if (trashItems.length === 0) {
259
+ listEl.innerHTML = '<div class="tw-empty-list">Trash is empty</div>';
260
+ renderTrashDetail();
261
+ return;
262
+ }
263
+
264
+ const filtered = getFilteredItems();
265
+
266
+ if (filtered.length === 0) {
267
+ listEl.innerHTML = '<div class="tw-empty-list">No matches</div>';
268
+ renderTrashDetail();
269
+ return;
270
+ }
271
+
272
+ // Group by category
273
+ const groups = {};
274
+ for (const item of filtered) {
275
+ const cat = item.payload.category || 'uncategorized';
276
+ if (!groups[cat]) groups[cat] = [];
277
+ groups[cat].push(item);
278
+ }
279
+
280
+ let html = '';
281
+ const sortedCats = Object.keys(groups).sort();
282
+ for (const cat of sortedCats) {
283
+ const items = groups[cat];
284
+ html += `<div class="tw-group">
285
+ <div class="tw-group-header">
286
+ <span class="tw-group-title">${esc(cat)}</span>
287
+ <span class="tw-group-count">${items.length}</span>
288
+ </div>`;
289
+ for (const item of items) {
290
+ const p = item.payload;
291
+ const preview = (p.content || '').replace(/\n/g, ' ').slice(0, 80);
292
+ const age = p.trashed_at ? formatTrashAge(p.trashed_at) : '';
293
+ const active = item.id === trashSelectedId ? ' active' : '';
294
+ html += `<div class="tw-item${active}" data-id="${item.id}">
295
+ <div class="tw-item-info">
296
+ <div class="tw-item-preview">${esc(preview) || '<span class="tw-faint">(empty)</span>'}</div>
297
+ ${age ? `<div class="tw-item-meta"><span class="tw-item-age">${age}</span></div>` : ''}
298
+ </div>
299
+ </div>`;
300
+ }
301
+ html += '</div>';
302
+ }
303
+
304
+ listEl.innerHTML = html;
305
+
306
+ // Wire clicks
307
+ listEl.querySelectorAll('.tw-item').forEach(el => {
308
+ el.addEventListener('click', () => {
309
+ trashSelectedId = el.dataset.id;
310
+ listEl.querySelectorAll('.tw-item').forEach(i => i.classList.remove('active'));
311
+ el.classList.add('active');
312
+ renderTrashDetail();
313
+ });
314
+ });
315
+ }
316
+
317
+ // ─── Render: Detail ──────────────────────
318
+
319
+ function renderTrashDetail() {
320
+ const win = $('trash-window');
321
+ if (!win) return;
322
+ const detailEl = win.querySelector('#tw-detail');
323
+ const item = trashItems.find(i => i.id === trashSelectedId);
324
+ if (!item) {
325
+ detailEl.innerHTML = '<div class="tw-empty">Select a memory to preview</div>';
326
+ return;
327
+ }
328
+
329
+ const p = item.payload;
330
+ const age = p.trashed_at ? formatTrashAge(p.trashed_at) : '';
331
+ const tags = (p.tags || []);
332
+ const tagHtml = tags.map(t => `<span class="tw-badge tw-badge--tag">${esc(t)}</span>`).join('');
333
+
334
+ detailEl.innerHTML = `
335
+ <div class="tw-detail-header">
336
+ <div class="tw-detail-meta-row">
337
+ <span class="tw-badge tw-badge--cat">${esc(p.category || 'uncategorized')}</span>
338
+ ${p.subcategory ? `<span class="tw-badge tw-badge--sub">${esc(p.subcategory)}</span>` : ''}
339
+ ${p.project ? `<span class="tw-badge tw-badge--project">${esc(p.project)}</span>` : ''}
340
+ ${age ? `<span class="tw-detail-age">trashed ${age}</span>` : ''}
341
+ </div>
342
+ <div class="tw-detail-actions">
343
+ <button class="tw-header-btn tw-restore-btn" id="tw-restore">
344
+ <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 6.03A7 7 0 1 1 1.05 10"/></svg>
345
+ Restore
346
+ </button>
347
+ <button class="tw-header-btn tw-delete-btn" id="tw-delete">
348
+ <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 3 14 13 14 13 6"/><line x1="1" y1="4" x2="15" y2="4"/><path d="M6 4V2h4v2"/><line x1="6" y1="8" x2="6" y2="11"/><line x1="10" y1="8" x2="10" y2="11"/></svg>
349
+ Delete
350
+ </button>
351
+ </div>
352
+ </div>
353
+ ${tags.length ? `<div class="tw-detail-tags">${tagHtml}</div>` : ''}
354
+ <div class="tw-detail-body">${formatMemoryContent(p.content || '')}</div>
355
+ ${p.related_files?.length ? `<div class="tw-detail-files"><span class="tw-detail-files-label">Related files</span>${p.related_files.map(f => `<span class="tw-detail-file">${esc(f)}</span>`).join('')}</div>` : ''}
356
+ `;
357
+
358
+ // Wire restore
359
+ detailEl.querySelector('#tw-restore').addEventListener('click', async () => {
360
+ const btn = detailEl.querySelector('#tw-restore');
361
+ btn.textContent = 'Restoring…';
362
+ btn.disabled = true;
363
+ try {
364
+ await restoreFromTrash(item.id);
365
+ trashItems = trashItems.filter(i => i.id !== item.id);
366
+ trashSelectedId = null;
367
+ updateTrashBadge();
368
+ renderTrashFilters();
369
+ renderTrashList();
370
+ renderTrashDetail();
371
+ emit('graph:reload');
372
+ emit('trash:updated');
373
+ } catch (err) { console.error('Restore error:', err); }
374
+ });
375
+
376
+ // Wire permanent delete
377
+ detailEl.querySelector('#tw-delete').addEventListener('click', async () => {
378
+ if (!confirm('Permanently delete this memory? This cannot be undone.')) return;
379
+ const btn = detailEl.querySelector('#tw-delete');
380
+ btn.textContent = 'Deleting…';
381
+ btn.disabled = true;
382
+ try {
383
+ await deleteMemoryPermanent(item.id);
384
+ trashItems = trashItems.filter(i => i.id !== item.id);
385
+ trashSelectedId = null;
386
+ updateTrashBadge();
387
+ renderTrashFilters();
388
+ renderTrashList();
389
+ renderTrashDetail();
390
+ emit('trash:updated');
391
+ } catch (err) { console.error('Permanent delete error:', err); }
392
+ });
393
+ }
394
+
395
+ // ─── Toggle (titlebar button) ────────────
396
+
397
+ function toggleTrashPanel(e) {
398
+ if (e) e.stopPropagation();
399
+ if (trashPanelOpen) closeTrashPanel();
400
+ else openTrashPanel();
401
+ }
402
+
403
+ // ─── Init ────────────────────────────────
404
+
405
+ /**
406
+ * Wire the titlebar trash button, subscribe to events, and fetch initial count.
407
+ * Call once after DOM is ready.
408
+ */
409
+ export function initTrash() {
410
+ // Titlebar / menubar button
411
+ const trashBtn = $('topright-trash-btn') || $('titlebar-trash-btn') || $('menubar-trash-btn');
412
+ if (trashBtn) {
413
+ trashBtn.addEventListener('click', toggleTrashPanel);
414
+ }
415
+
416
+ // External open request (e.g. from navbar module)
417
+ on('trash:open', () => {
418
+ if (!trashPanelOpen) openTrashPanel();
419
+ });
420
+
421
+ // Fetch initial trash count for badge
422
+ fetchTrashItems();
423
+ }
424
+
425
+ // ─── Public API ──────────────────────────
426
+
427
+ export {
428
+ openTrashPanel,
429
+ closeTrashPanel,
430
+ fetchTrashItems,
431
+ };
432
+
433
+ /**
434
+ * Check whether the trash panel is currently open.
435
+ * @returns {boolean}
436
+ */
437
+ export function isTrashOpen() {
438
+ return trashPanelOpen;
439
+ }