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.
- package/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- 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">×</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|
+
}
|