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,653 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Resume Sessions
3
+ // Toolbar dropdown for browsing and resuming
4
+ // past Claude Code sessions across projects.
5
+ // ═══════════════════════════════════════════
6
+
7
+ import { emit, on } from './state.js';
8
+ import { fetchClaudeSessions, searchMemories, startSessionIndexing, cancelSessionIndexing, fetchIndexingStatus } from './api.js';
9
+ import { isGuest, hasPermission } from './ui-sync.js';
10
+ import { storage } from './storage.js';
11
+
12
+ const $ = (id) => document.getElementById(id);
13
+
14
+ // ── State ──
15
+ let _cachedProjects = null; // null = never loaded
16
+ let _searchDebounce = null;
17
+ let _currentSearch = '';
18
+ let _expandedProject = null; // path of currently expanded project
19
+ let _loading = false;
20
+
21
+ // ── Indexing state ──
22
+ let _indexing = false;
23
+ let _indexingProgress = { completed: 0, total: 0, chunks: 0 };
24
+ let _indexedSessionIds = new Set();
25
+ let _indexingStatusLoaded = false;
26
+
27
+ // ── Session labels (server-synced via storage.js) ──
28
+ const LABEL_PREFIX = 'synabun-session-label:';
29
+
30
+ function getSessionLabel(sessionId) {
31
+ try { return storage.getItem(LABEL_PREFIX + sessionId) || ''; } catch { return ''; }
32
+ }
33
+
34
+ function setSessionLabel(sessionId, label) {
35
+ try {
36
+ if (label) storage.setItem(LABEL_PREFIX + sessionId, label);
37
+ else storage.removeItem(LABEL_PREFIX + sessionId);
38
+ } catch { /* storage error */ }
39
+ }
40
+
41
+ // ── Public API ──
42
+
43
+ export function initResume() {
44
+ on('resume:render', renderResume);
45
+
46
+ // Subscribe to indexing WebSocket events
47
+ on('ws:message', (msg) => {
48
+ if (!msg || !msg.type || !msg.type.startsWith('indexing:')) return;
49
+ handleIndexingEvent(msg);
50
+ });
51
+ }
52
+
53
+ function handleIndexingEvent(event) {
54
+ switch (event.type) {
55
+ case 'indexing:started':
56
+ _indexing = true;
57
+ _indexingProgress = { completed: 0, total: event.totalSessions, chunks: 0 };
58
+ updateIndexingUI();
59
+ break;
60
+ case 'indexing:session-complete':
61
+ _indexingProgress.completed = (event.sessionIndex ?? _indexingProgress.completed) + 1;
62
+ _indexingProgress.chunks += event.chunkCount || 0;
63
+ updateIndexingUI();
64
+ break;
65
+ case 'indexing:complete':
66
+ _indexing = false;
67
+ _indexingProgress = { completed: event.totalSessions, total: event.totalSessions, chunks: event.totalChunks };
68
+ // Refresh indexed session IDs
69
+ loadIndexingStatus();
70
+ updateIndexingUI();
71
+ break;
72
+ case 'indexing:cancelled':
73
+ _indexing = false;
74
+ loadIndexingStatus();
75
+ updateIndexingUI();
76
+ break;
77
+ case 'indexing:error':
78
+ // Non-fatal — just update UI
79
+ updateIndexingUI();
80
+ break;
81
+ }
82
+ }
83
+
84
+ function updateIndexingUI() {
85
+ const bar = document.querySelector('.resume-indexing-bar');
86
+ if (!bar) return;
87
+
88
+ if (_indexing) {
89
+ const pct = _indexingProgress.total > 0 ? Math.round((_indexingProgress.completed / _indexingProgress.total) * 100) : 0;
90
+ bar.style.display = '';
91
+ bar.innerHTML = `
92
+ <div class="resume-indexing-progress">
93
+ <div class="resume-indexing-fill" style="width: ${pct}%"></div>
94
+ </div>
95
+ <div class="resume-indexing-text">
96
+ Indexing ${_indexingProgress.completed}/${_indexingProgress.total} sessions (${_indexingProgress.chunks} chunks)
97
+ <button class="resume-indexing-cancel" title="Cancel indexing">&times;</button>
98
+ </div>
99
+ `;
100
+ const cancelBtn = bar.querySelector('.resume-indexing-cancel');
101
+ if (cancelBtn) {
102
+ cancelBtn.addEventListener('click', async (e) => {
103
+ e.stopPropagation();
104
+ try { await cancelSessionIndexing(); } catch {}
105
+ });
106
+ }
107
+ } else {
108
+ bar.style.display = 'none';
109
+ bar.innerHTML = '';
110
+ }
111
+ }
112
+
113
+ async function loadIndexingStatus() {
114
+ try {
115
+ const status = await fetchIndexingStatus();
116
+ _indexing = !!status.running;
117
+ _indexedSessionIds = new Set(status.indexedSessionIds || []);
118
+ if (status.progress) _indexingProgress = status.progress;
119
+ _indexingStatusLoaded = true;
120
+ } catch { /* ignore */ }
121
+ }
122
+
123
+ // ── Core render ──
124
+
125
+ async function renderResume() {
126
+ const container = $('resume-list');
127
+ if (!container) return;
128
+
129
+ if (isGuest() && !hasPermission('terminal')) {
130
+ container.innerHTML = '<div class="resume-empty">Terminal access required.</div>';
131
+ return;
132
+ }
133
+
134
+ // Always reload on menu open to catch new sessions
135
+ container.innerHTML = '<div class="resume-empty resume-loading">Loading sessions...</div>';
136
+ await loadAllProjects(_currentSearch);
137
+
138
+ renderProjectList(container);
139
+ }
140
+
141
+ async function loadAllProjects(search = '') {
142
+ _loading = true;
143
+ try {
144
+ const data = await fetchClaudeSessions({ limit: 50, search });
145
+ _cachedProjects = data.projects || [];
146
+ } catch (err) {
147
+ console.error('[resume] Failed to load sessions:', err);
148
+ _cachedProjects = [];
149
+ }
150
+ _loading = false;
151
+ }
152
+
153
+ // ── Render ──
154
+
155
+ function renderProjectList(container) {
156
+ // Preserve existing search input across re-renders to keep cursor position
157
+ let searchBox = container.querySelector('.resume-search');
158
+
159
+ // Remove everything except the search box
160
+ [...container.children].forEach(el => {
161
+ if (!el.classList.contains('resume-search')) el.remove();
162
+ });
163
+
164
+ // Load indexing status on first render
165
+ if (!_indexingStatusLoaded) {
166
+ loadIndexingStatus().then(() => {
167
+ updateIndexingUI();
168
+ // Re-render session list to show indexed badges
169
+ const list = container.querySelector('.resume-session-list');
170
+ if (list && _indexedSessionIds.size > 0) renderProjectList(container);
171
+ });
172
+ }
173
+
174
+ // Create search box only on first render
175
+ if (!searchBox) {
176
+ searchBox = document.createElement('div');
177
+ searchBox.className = 'resume-search';
178
+ searchBox.innerHTML = `
179
+ <div class="resume-search-wrap">
180
+ <svg class="resume-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
181
+ <input type="text" class="resume-search-input"
182
+ placeholder="Search past sessions..."
183
+ value="${escHtml(_currentSearch)}"
184
+ autocomplete="off" spellcheck="false">
185
+ <button class="resume-refresh-btn" title="Refresh session list">
186
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/></svg>
187
+ </button>
188
+ <button class="resume-index-btn" title="Index sessions for deep search">
189
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
190
+ </button>
191
+ </div>
192
+ <div class="resume-indexing-bar" style="display:none"></div>
193
+ `;
194
+ container.prepend(searchBox);
195
+
196
+ const input = searchBox.querySelector('.resume-search-input');
197
+ input.addEventListener('input', (e) => {
198
+ _currentSearch = e.target.value;
199
+ clearTimeout(_searchDebounce);
200
+ _searchDebounce = setTimeout(() => performSearch(_currentSearch, container), 300);
201
+ });
202
+ input.addEventListener('click', (e) => e.stopPropagation());
203
+ input.addEventListener('keydown', (e) => e.stopPropagation());
204
+
205
+ // Refresh button handler
206
+ const refreshBtn = searchBox.querySelector('.resume-refresh-btn');
207
+ refreshBtn.addEventListener('click', async (e) => {
208
+ e.stopPropagation();
209
+ refreshBtn.classList.add('resume-refresh-spinning');
210
+ try {
211
+ const data = await fetchClaudeSessions({ limit: 50, search: _currentSearch, refresh: true });
212
+ _cachedProjects = data.projects || [];
213
+ } catch (err) {
214
+ console.error('[resume] Refresh failed:', err);
215
+ }
216
+ refreshBtn.classList.remove('resume-refresh-spinning');
217
+ renderProjectList(container);
218
+ });
219
+
220
+ // Index button handler
221
+ const indexBtn = searchBox.querySelector('.resume-index-btn');
222
+ indexBtn.addEventListener('click', async (e) => {
223
+ e.stopPropagation();
224
+ if (_indexing) return;
225
+ try {
226
+ // Get the currently expanded project, or index all
227
+ const project = _expandedProject || undefined;
228
+ await startSessionIndexing({ project });
229
+ _indexing = true;
230
+ _indexingProgress = { completed: 0, total: 0, chunks: 0 };
231
+ updateIndexingUI();
232
+ } catch (err) {
233
+ console.error('[resume] Failed to start indexing:', err);
234
+ }
235
+ });
236
+
237
+ requestAnimationFrame(() => input.focus());
238
+ }
239
+
240
+ if (!_cachedProjects || _cachedProjects.length === 0) {
241
+ const empty = document.createElement('div');
242
+ empty.className = 'resume-empty';
243
+ empty.textContent = _loading ? 'Loading...' : 'No projects registered.';
244
+ container.appendChild(empty);
245
+ return;
246
+ }
247
+
248
+ // "Continue last session" — most recent across all projects
249
+ const allSessions = _cachedProjects
250
+ .flatMap(p => p.sessions.map(s => ({ ...s, projectPath: p.path, projectLabel: p.label })))
251
+ .sort((a, b) => new Date(b.modified) - new Date(a.modified));
252
+
253
+ if (allSessions.length > 0 && !_currentSearch) {
254
+ // Find the most recent session with actual content (skip empty/tag-only/deleted sessions)
255
+ const latest = allSessions.find(s => {
256
+ if (s.deleted) return false;
257
+ const label = getSessionLabel(s.sessionId);
258
+ if (label) return true;
259
+ const cleaned = cleanPrompt(s.firstPrompt);
260
+ return cleaned && cleaned.length > 0;
261
+ }) || allSessions.find(s => !s.deleted) || allSessions[0]; // fallback to newest non-deleted
262
+
263
+ const previewText = getSessionLabel(latest.sessionId) || cleanPrompt(latest.firstPrompt) || '';
264
+
265
+ const continueEl = document.createElement('div');
266
+ continueEl.className = 'resume-continue';
267
+ continueEl.innerHTML = `
268
+ <div class="resume-continue-row">
269
+ <svg class="resume-continue-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
270
+ <div class="resume-continue-text">
271
+ <div class="resume-continue-prompt">${previewText ? escHtml(trunc(previewText, 70)) : '<span class="resume-continue-fallback">Resume most recent</span>'}</div>
272
+ <div class="resume-continue-meta">
273
+ <span class="resume-project-badge">${escHtml(latest.projectLabel)}</span>
274
+ <span class="resume-meta">${relDate(latest.modified)}</span>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ `;
279
+ continueEl.addEventListener('click', () => {
280
+ closeMenu();
281
+ launchResume(latest.sessionId, latest.projectPath);
282
+ });
283
+ container.appendChild(continueEl);
284
+ container.appendChild(sep());
285
+ }
286
+
287
+ // Memory search results (only when actively searching)
288
+ if (_currentSearch) {
289
+ const memSlot = document.createElement('div');
290
+ memSlot.id = 'resume-memory-results';
291
+ container.appendChild(memSlot);
292
+ searchMemoryResults(_currentSearch, memSlot);
293
+ }
294
+
295
+ // Project groups
296
+ _cachedProjects.forEach(proj => {
297
+ const group = document.createElement('div');
298
+ group.className = 'resume-project-group';
299
+
300
+ // Header
301
+ const header = document.createElement('div');
302
+ header.className = 'resume-project-header';
303
+ const isExpanded = _expandedProject === proj.path || !!_currentSearch;
304
+ header.innerHTML = `
305
+ <span class="resume-project-chevron">${isExpanded ? '\u25BC' : '\u25B6'}</span>
306
+ <span class="resume-project-name">${escHtml(proj.label)}</span>
307
+ <span class="resume-project-count">${proj.total}</span>
308
+ `;
309
+ header.addEventListener('click', (e) => {
310
+ e.stopPropagation();
311
+ _expandedProject = _expandedProject === proj.path ? null : proj.path;
312
+ renderProjectList(container);
313
+ });
314
+ group.appendChild(header);
315
+
316
+ // Session list (expanded)
317
+ if (isExpanded) {
318
+ const list = document.createElement('div');
319
+ list.className = 'resume-session-list';
320
+
321
+ if (proj.sessions.length === 0) {
322
+ list.innerHTML = '<div class="resume-no-sessions">No matching sessions</div>';
323
+ } else {
324
+ // Group sessions by time period
325
+ let lastGroup = '';
326
+ proj.sessions.forEach(s => {
327
+ const group = timeGroup(s.modified);
328
+ if (group !== lastGroup) {
329
+ lastGroup = group;
330
+ const label = document.createElement('div');
331
+ label.className = 'resume-time-group';
332
+ label.textContent = group;
333
+ list.appendChild(label);
334
+ }
335
+ list.appendChild(renderSession(s, proj.path));
336
+ });
337
+
338
+ // "Load more" if paginated
339
+ if (proj.sessions.length < proj.total) {
340
+ const more = document.createElement('div');
341
+ more.className = 'resume-load-more';
342
+ more.textContent = `Show more (${proj.total - proj.sessions.length} remaining)`;
343
+ more.addEventListener('click', async (e) => {
344
+ e.stopPropagation();
345
+ more.textContent = 'Loading...';
346
+ try {
347
+ const data = await fetchClaudeSessions({
348
+ project: proj.path,
349
+ limit: 30,
350
+ offset: proj.sessions.length,
351
+ search: _currentSearch,
352
+ });
353
+ const extra = data.projects?.[0]?.sessions || [];
354
+ proj.sessions.push(...extra);
355
+ renderProjectList(container);
356
+ } catch { more.textContent = 'Failed to load'; }
357
+ });
358
+ list.appendChild(more);
359
+ }
360
+ }
361
+
362
+ group.appendChild(list);
363
+ }
364
+
365
+ container.appendChild(group);
366
+ });
367
+ }
368
+
369
+ function renderSession(s, projectPath) {
370
+ const item = document.createElement('div');
371
+ item.className = 'resume-session-item';
372
+
373
+ const branchHtml = s.gitBranch
374
+ ? `<span class="resume-branch">${escHtml(s.gitBranch)}</span>` : '';
375
+
376
+ const customLabel = getSessionLabel(s.sessionId);
377
+ const cleaned = cleanPrompt(s.firstPrompt);
378
+ const displayText = customLabel || (cleaned ? trunc(cleaned, 100) : '');
379
+ const isEmpty = !displayText;
380
+ const promptClass = isEmpty ? 'resume-session-prompt resume-session-prompt--empty' : 'resume-session-prompt';
381
+ const promptText = isEmpty ? 'Empty session' : escHtml(displayText);
382
+
383
+ // Pencil icon (rename) — appears on hover via CSS
384
+ const renameBtn = `<button class="resume-rename-btn" title="Rename session"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg></button>`;
385
+
386
+ // Message count visual — icon + count
387
+ const msgLabel = s.messageCount === 0 ? '' :
388
+ `<span class="resume-meta resume-msg-count">${s.messageCount} msg${s.messageCount !== 1 ? 's' : ''}</span>`;
389
+
390
+ // Indexed badge
391
+ const indexedBadge = _indexedSessionIds.has(s.sessionId)
392
+ ? '<span class="resume-indexed-badge" title="Indexed for deep search">indexed</span>'
393
+ : '';
394
+
395
+ item.innerHTML = `
396
+ <div class="${promptClass}" data-prompt-display>${promptText}</div>
397
+ <div class="resume-session-meta">
398
+ ${branchHtml}
399
+ <span class="resume-meta">${relDate(s.modified)}</span>
400
+ ${msgLabel}
401
+ ${indexedBadge}
402
+ </div>
403
+ ${renameBtn}
404
+ `;
405
+
406
+ // Dim empty sessions
407
+ if (s.messageCount === 0 && isEmpty) {
408
+ item.classList.add('resume-session-item--empty');
409
+ }
410
+
411
+ // Dim deleted sessions (file removed by Claude Code, cached in SynaBun)
412
+ if (s.deleted) {
413
+ item.classList.add('resume-session-item--deleted');
414
+ item.title = 'Session file was cleaned up by Claude Code — no longer resumable';
415
+ }
416
+
417
+ // Rename button handler
418
+ const renBtn = item.querySelector('.resume-rename-btn');
419
+ renBtn.addEventListener('click', (e) => {
420
+ e.stopPropagation();
421
+ startRename(item, s);
422
+ });
423
+
424
+ // Launch on click (but not when renaming)
425
+ item.addEventListener('click', (e) => {
426
+ if (item.querySelector('.resume-rename-input')) return; // renaming in progress
427
+ if (s.deleted) return; // can't resume deleted sessions
428
+ closeMenu();
429
+ launchResume(s.sessionId, projectPath);
430
+ });
431
+
432
+ return item;
433
+ }
434
+
435
+ function startRename(item, session) {
436
+ // Already renaming?
437
+ if (item.querySelector('.resume-rename-input')) return;
438
+
439
+ const promptEl = item.querySelector('[data-prompt-display]');
440
+ const renameBtn = item.querySelector('.resume-rename-btn');
441
+ if (!promptEl) return;
442
+
443
+ // Hide rename button while editing
444
+ if (renameBtn) renameBtn.style.display = 'none';
445
+
446
+ const currentLabel = getSessionLabel(session.sessionId) || cleanPrompt(session.firstPrompt) || '';
447
+ const input = document.createElement('input');
448
+ input.type = 'text';
449
+ input.className = 'resume-rename-input';
450
+ input.value = currentLabel;
451
+ input.placeholder = cleanPrompt(session.firstPrompt) ? trunc(cleanPrompt(session.firstPrompt), 60) : 'Session name...';
452
+
453
+ // Replace prompt text with input
454
+ promptEl.style.display = 'none';
455
+ promptEl.parentNode.insertBefore(input, promptEl);
456
+
457
+ // Prevent menu close and event bubbling
458
+ input.addEventListener('click', (e) => e.stopPropagation());
459
+ input.addEventListener('keydown', (e) => {
460
+ e.stopPropagation();
461
+ if (e.key === 'Enter') commitRename(input, promptEl, session, renameBtn);
462
+ if (e.key === 'Escape') cancelRename(input, promptEl, renameBtn);
463
+ });
464
+ input.addEventListener('blur', () => commitRename(input, promptEl, session, renameBtn));
465
+
466
+ requestAnimationFrame(() => { input.focus(); input.select(); });
467
+ }
468
+
469
+ function commitRename(input, promptEl, session, renameBtn) {
470
+ if (!input.parentNode) return; // already committed
471
+ const value = input.value.trim();
472
+
473
+ // Save (or clear if matches original or empty)
474
+ const originalClean = cleanPrompt(session.firstPrompt);
475
+ if (value && value !== originalClean) {
476
+ setSessionLabel(session.sessionId, value);
477
+ } else {
478
+ setSessionLabel(session.sessionId, ''); // clear custom label
479
+ }
480
+
481
+ // Update display
482
+ const displayText = value || (originalClean ? trunc(originalClean, 100) : '');
483
+ if (displayText) {
484
+ promptEl.className = 'resume-session-prompt';
485
+ promptEl.textContent = displayText;
486
+ } else {
487
+ promptEl.className = 'resume-session-prompt resume-session-prompt--empty';
488
+ promptEl.textContent = 'No prompt';
489
+ }
490
+
491
+ promptEl.style.display = '';
492
+ input.remove();
493
+ if (renameBtn) renameBtn.style.display = '';
494
+ }
495
+
496
+ function cancelRename(input, promptEl, renameBtn) {
497
+ if (!input.parentNode) return;
498
+ promptEl.style.display = '';
499
+ input.remove();
500
+ if (renameBtn) renameBtn.style.display = '';
501
+ }
502
+
503
+ // ── Memory search integration ──
504
+
505
+ async function searchMemoryResults(query, container) {
506
+ try {
507
+ const data = await searchMemories(query, 5);
508
+ if (!data.results || data.results.length === 0) return;
509
+
510
+ // Filter to conversation memories
511
+ const convResults = data.results.filter(r =>
512
+ r.payload?.category === 'conversations'
513
+ );
514
+ if (convResults.length === 0) return;
515
+
516
+ const header = document.createElement('div');
517
+ header.className = 'resume-group-label';
518
+ header.textContent = 'Related Memories';
519
+ container.appendChild(header);
520
+
521
+ convResults.forEach(r => {
522
+ const content = r.payload?.content || '';
523
+ const project = r.payload?.project;
524
+
525
+ // Try to extract session ID from memory content
526
+ const sidMatch = content.match(/Session ID:\s*([0-9a-f-]{36})/i)
527
+ || content.match(/\*\*Session ID\*\*:\s*([0-9a-f-]{36})/i);
528
+ const sessionId = sidMatch?.[1] || r.payload?.session_id;
529
+
530
+ // Extract first line as title
531
+ const firstLine = content.split('\n').find(l => l.trim()) || content;
532
+
533
+ const item = document.createElement('div');
534
+ item.className = 'resume-memory-item';
535
+ item.innerHTML = `
536
+ <div class="resume-session-prompt">${escHtml(trunc(firstLine, 80))}</div>
537
+ <div class="resume-session-meta">
538
+ <span class="resume-meta">${relDate(r.payload?.created_at)}</span>
539
+ ${project ? `<span class="resume-project-badge">${escHtml(project)}</span>` : ''}
540
+ </div>
541
+ `;
542
+
543
+ if (sessionId) {
544
+ item.style.cursor = 'pointer';
545
+ item.addEventListener('click', () => {
546
+ closeMenu();
547
+ // Find matching project
548
+ const proj = _cachedProjects?.find(p =>
549
+ p.label?.toLowerCase() === project?.toLowerCase() ||
550
+ p.path?.toLowerCase().endsWith(project?.toLowerCase())
551
+ );
552
+ if (proj) launchResume(sessionId, proj.path);
553
+ });
554
+ }
555
+
556
+ container.appendChild(item);
557
+ });
558
+ } catch (err) {
559
+ console.warn('[resume] Memory search failed:', err);
560
+ }
561
+ }
562
+
563
+ // ── Search ──
564
+
565
+ async function performSearch(query, container) {
566
+ if (query) {
567
+ await loadAllProjects(query);
568
+ } else {
569
+ _cachedProjects = null; // force reload without filter
570
+ await loadAllProjects();
571
+ }
572
+ renderProjectList(container);
573
+ }
574
+
575
+ // ── Terminal launch ──
576
+
577
+ async function launchResume(claudeSessionId, projectPath) {
578
+ const label = getSessionLabel(claudeSessionId);
579
+ emit('terminal:open-resume', {
580
+ profile: 'claude-code',
581
+ cwd: projectPath,
582
+ resume: claudeSessionId,
583
+ label: label || '',
584
+ });
585
+ }
586
+
587
+ // ── Helpers ──
588
+
589
+ function closeMenu() {
590
+ emit('panel:close-all-dropdowns');
591
+ }
592
+
593
+ function trunc(str, max) {
594
+ if (!str) return '';
595
+ return str.length > max ? str.slice(0, max) + '...' : str;
596
+ }
597
+
598
+ function escHtml(str) {
599
+ if (!str) return '';
600
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
601
+ }
602
+
603
+ /** Strip XML-like tags (ide_opened_file, system-reminder, ide_selection, etc.) and clean up prompt text */
604
+ function cleanPrompt(raw) {
605
+ if (!raw) return '';
606
+ let s = raw
607
+ // Remove full XML tags with content on the same line: <tag>...</tag>
608
+ .replace(/<\/?(?:ide_opened_file|ide_selection|system-reminder|antml:[a-z_]+)[^>]*>[^\n]*/gi, '')
609
+ // Remove any remaining self-closing or opening/closing XML-style tags
610
+ .replace(/<\/?[a-z_-]+[^>]*>/gi, '')
611
+ // Collapse whitespace
612
+ .replace(/\s+/g, ' ')
613
+ .trim();
614
+ return s;
615
+ }
616
+
617
+ function relDate(dateStr) {
618
+ if (!dateStr) return '';
619
+ const d = new Date(dateStr);
620
+ const now = Date.now();
621
+ const diff = now - d.getTime();
622
+ const min = Math.floor(diff / 60000);
623
+ const hr = Math.floor(diff / 3600000);
624
+ const day = Math.floor(diff / 86400000);
625
+
626
+ if (min < 1) return 'just now';
627
+ if (min < 60) return `${min}m ago`;
628
+ if (hr < 24) return `${hr}h ago`;
629
+ if (day < 7) return `${day}d ago`;
630
+ if (day < 30) return `${Math.floor(day / 7)}w ago`;
631
+ return d.toLocaleDateString();
632
+ }
633
+
634
+ /** Get time-group label for a date */
635
+ function timeGroup(dateStr) {
636
+ if (!dateStr) return 'Older';
637
+ const d = new Date(dateStr);
638
+ const now = new Date();
639
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
640
+ const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1);
641
+ const weekAgo = new Date(today); weekAgo.setDate(today.getDate() - 7);
642
+
643
+ if (d >= today) return 'Today';
644
+ if (d >= yesterday) return 'Yesterday';
645
+ if (d >= weekAgo) return 'This Week';
646
+ return 'Older';
647
+ }
648
+
649
+ function sep() {
650
+ const el = document.createElement('div');
651
+ el.className = 'menu-sep';
652
+ return el;
653
+ }