Squash Odysseus development history

This commit is contained in:
pewdiepie-archdaemon
2026-09-11 06:04:19 +00:00
parent e5c99a5eee
commit 6ee6502010
2050 changed files with 538359 additions and 57745 deletions
+307 -39
View File
@@ -1,11 +1,14 @@
// Memory Management Functions
// This module handles all memory-related operations
import uiModule from './ui.js';
import uiModule from './ui.js?v=20260908weekhoverfix1';
import sessionModule from './sessions.js';
import spinnerModule from './spinner.js';
import { makeWindowDraggable } from './windowDrag.js';
import { snapModalToZone } from './tileManager.js';
import { snapModalToZone } from './tileManager.js?v=20260910responsivebounds1';
import { topPortalZ } from './toolWindowZOrder.js';
import { orderActionMenuItems, actionMenuRank, SELECT_MENU_ICON } from './actionMenuOrder.js';
import { bindMenuDismiss } from './escMenuStack.js';
var escapeHtml = uiModule.esc;
@@ -14,10 +17,108 @@ let activeCategory = 'all';
let sortOrder = 'newest';
let selectMode = false;
let selectedIds = new Set();
let memoriesLoading = false;
let activeMemoryModalMode = 'memory';
const MEMORY_CATEGORIES = ['fact', 'identity', 'preference', 'contact', 'project', 'goal', 'task'];
// Sort-option icons for the custom Memory sort picker (and Skills picker
// once it reuses the same markup). Each value maps to a 13px Feather-style
// SVG so the icon visually distinguishes Newest / Oldest / A-Z / Most used.
const _MEMORY_SORT_ICONS = {
newest: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
oldest: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><polyline points="3 3 3 8 8 8"/><polyline points="12 7 12 12 16 14"/></svg>',
alpha: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h6"/><path d="M3 10h6"/><path d="M3 16h4"/><path d="M14 4l4 12"/><path d="M16 12h4"/><polyline points="17 18 21 14 17 10"/><line x1="21" y1="14" x2="13" y2="14"/></svg>',
uses: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>',
};
function _memorySortIcon(value) {
return _MEMORY_SORT_ICONS[value] || _MEMORY_SORT_ICONS.newest;
}
function _renderMemorySortPickerCurrent() {
const sel = document.getElementById('memory-sort');
const btn = document.getElementById('memory-sort-btn');
if (!sel || !btn) return;
const value = sel.value || 'newest';
const opt = sel.querySelector(`option[value="${CSS.escape(value)}"]`);
const label = opt ? opt.textContent : value;
const iconWrap = btn.querySelector('.memory-sort-icon-cur');
const labelEl = btn.querySelector('.memory-sort-label');
if (iconWrap) iconWrap.innerHTML = _memorySortIcon(value);
if (labelEl) labelEl.textContent = label;
}
function _initMemorySortPicker() {
const sel = document.getElementById('memory-sort');
const picker = document.getElementById('memory-sort-picker');
const btn = document.getElementById('memory-sort-btn');
const menu = document.getElementById('memory-sort-menu');
if (!sel || !picker || !btn || !menu || picker._wired) return;
picker._wired = true;
const items = Array.from(sel.children)
.filter(o => o.tagName === 'OPTION')
.map(o => ({ value: o.value, label: o.textContent }));
menu.innerHTML = items.map(it => `
<button type="button" role="option" class="memory-sort-item" data-value="${it.value}">
<span class="memory-sort-item-icon">${_memorySortIcon(it.value)}</span>
<span class="memory-sort-item-label">${it.label}</span>
</button>
`).join('');
let unregister = () => {};
const close = () => {
menu.hidden = true;
btn.setAttribute('aria-expanded', 'false');
unregister();
unregister = () => {};
};
const open = () => { menu.hidden = false; btn.setAttribute('aria-expanded', 'true'); };
btn.addEventListener('click', (e) => {
e.stopPropagation();
if (menu.hidden) {
open();
unregister = bindMenuDismiss(menu, close, e => picker.contains(e.target));
} else close();
});
menu.addEventListener('click', (e) => {
const item = e.target.closest('.memory-sort-item');
if (!item) return;
sel.value = item.dataset.value;
sel.dispatchEvent(new Event('change', { bubbles: true }));
_renderMemorySortPickerCurrent();
close();
});
document.addEventListener('click', (e) => {
if (!menu.hidden && !picker.contains(e.target)) close();
});
_renderMemorySortPickerCurrent();
}
function _ensureNewMemoryCategorySelect() {
const sel = document.getElementById('new-memory-category');
if (!sel || sel.dataset.wired === '1') return;
sel.dataset.wired = '1';
MEMORY_CATEGORIES.forEach(cat => {
const opt = document.createElement('option');
opt.value = cat;
opt.textContent = cat;
if (cat === 'fact') opt.selected = true;
sel.appendChild(opt);
});
}
function _readNewMemoryCategory() {
_ensureNewMemoryCategorySelect();
const sel = document.getElementById('new-memory-category');
const cat = sel?.value || 'fact';
return MEMORY_CATEGORIES.includes(cat) ? cat : 'fact';
}
let _memoryDragWired = false;
function _wireMemoryDrag() {
if (_memoryDragWired) return;
@@ -68,16 +169,25 @@ function buildCategoryChips() {
const cats = new Set(memories.map(m => m.category || 'fact'));
const sorted = ['all', ...Array.from(cats).sort()];
const counts = memories.reduce((result, memory) => {
const category = memory.category || 'fact';
result[category] = (result[category] || 0) + 1;
return result;
}, {});
container.innerHTML = '';
sorted.forEach(cat => {
const btn = document.createElement('button');
btn.className = 'memory-cat-chip' + (cat === activeCategory ? ' active' : '');
btn.className = 'skills-summary-chip memory-filter-chip' + (cat === activeCategory ? ' active' : '');
btn.dataset.cat = cat;
btn.textContent = cat;
const label = document.createElement('span');
label.textContent = cat === 'all' ? 'All' : cat;
const count = document.createElement('strong');
count.textContent = String(cat === 'all' ? memories.length : counts[cat] || 0);
btn.replaceChildren(label, count);
btn.addEventListener('click', () => {
activeCategory = cat;
container.querySelectorAll('.memory-cat-chip').forEach(b => b.classList.remove('active'));
container.querySelectorAll('[data-cat]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderMemoryList();
updateMemoryCount();
@@ -119,7 +229,10 @@ async function syncToggles() {
const skillsToggle = document.getElementById('skills-enabled-header-toggle');
if (skillsToggle) {
const skillsPanel = document.querySelector('[data-memory-panel="skills"]');
const applyDim = () => { if (skillsPanel) skillsPanel.style.opacity = skillsToggle.checked ? '' : '0.3'; };
const applyDim = () => {
if (skillsPanel) skillsPanel.style.opacity = skillsToggle.checked ? '' : '0.3';
reflectSkillsToggleInSidebar(skillsToggle.checked);
};
applyDim();
if (!skillsToggle.dataset.boundUx) {
skillsToggle.dataset.boundUx = '1';
@@ -128,9 +241,96 @@ async function syncToggles() {
}
}
function _applyMemoryModalMode(mode) {
activeMemoryModalMode = mode === 'skills' ? 'skills' : 'memory';
const modal = document.getElementById('memory-modal');
if (!modal) return;
modal.dataset.memoryMode = activeMemoryModalMode;
modal.setAttribute('aria-label', activeMemoryModalMode === 'skills' ? 'Skills' : 'Memory');
const titleText = document.getElementById('memory-modal-title-text');
if (titleText) titleText.textContent = activeMemoryModalMode === 'skills' ? 'Skills' : 'Memory';
modal.querySelectorAll('[data-memory-scope]').forEach(el => {
const scope = el.dataset.memoryScope;
const scopedOut = !!scope && scope !== activeMemoryModalMode;
el.hidden = scopedOut;
el.classList.toggle('hidden', scopedOut);
el.setAttribute('aria-hidden', scopedOut ? 'true' : 'false');
});
}
export function openMemoryModal(tabName = 'browse') {
const modal = document.getElementById('memory-modal');
if (!modal) return;
const mode = tabName === 'skills' ? 'skills' : 'memory';
_applyMemoryModalMode(mode);
modal.classList.remove('hidden', 'modal-minimized');
modal.style.display = '';
if (renderMemoryList) renderMemoryList();
if (updateMemoryCount) updateMemoryCount();
const target = mode === 'skills' ? 'skills' : 'browse';
const tab = document.querySelector(`.memory-tab[data-memory-tab="${target}"]`);
if (tab) {
tab.click();
} else if (target === 'skills') {
import('./skills.js?v=20260909kebabconsistency1').then(m => {
if (m.loadSkills) m.loadSkills(true);
else if (m.default?.loadSkills) m.default.loadSkills(true);
});
}
}
function _decodeMemoryAnchorId(memoryId) {
try { return decodeURIComponent(String(memoryId || '').replace(/\+/g, ' ')).trim(); }
catch (_) { return String(memoryId || '').trim(); }
}
function _resetMemoryDeepLinkFilters() {
activeCategory = 'all';
const search = document.getElementById('memory-search');
if (search && search.value) search.value = '';
document.querySelectorAll('#memory-category-filters [data-cat]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.cat === 'all');
});
}
// Open the memory panel and focus the memory referenced by a chat tool link.
// Memory search output may expose a shortened id, so prefix matching is
// intentional here.
export async function openMemory(memoryId, attempt = 0) {
openMemoryModal('browse');
_resetMemoryDeepLinkFilters();
await loadMemories();
if (renderMemoryList) renderMemoryList();
const wanted = _decodeMemoryAnchorId(memoryId);
if (!wanted) return;
const item = Array.from(document.querySelectorAll('.memory-item[data-memory-id]'))
.find(el => String(el.dataset.memoryId || '') === wanted)
|| Array.from(document.querySelectorAll('.memory-item[data-memory-id]'))
.find(el => String(el.dataset.memoryId || '').startsWith(wanted));
if (!item) {
if (attempt < 12) setTimeout(() => openMemory(wanted, attempt + 1), 120);
return;
}
item.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
item.classList.add('memory-item-focus');
setTimeout(() => item.classList.remove('memory-item-focus'), 2200);
}
function reflectMemoryToggleInSidebar(enabled) {
const btn = document.getElementById('tool-memory-btn');
if (btn) btn.classList.toggle('tool-disabled', !enabled);
document.body.classList.toggle('memory-disabled', !enabled);
if (!enabled) document.getElementById('synapse-canvas')?.remove();
window.dispatchEvent(new CustomEvent('memory-visual-state', { detail: { enabled } }));
['tool-memory-btn', 'rail-memory'].forEach(id => {
const btn = document.getElementById(id);
if (btn) btn.classList.toggle('tool-disabled', !enabled);
});
}
function reflectSkillsToggleInSidebar(enabled) {
['tool-skills-btn', 'rail-skills'].forEach(id => {
const btn = document.getElementById(id);
if (btn) btn.classList.toggle('tool-disabled', !enabled);
});
}
function syncToggleDim(toggle) {
@@ -274,12 +474,17 @@ async function syncPrefToggle(elementId, prefKey, onMsg, offMsg, dimBelow = true
}
export async function loadMemories() {
_ensureNewMemoryCategorySelect();
memoriesLoading = true;
renderMemoryList();
updateMemoryCount();
try {
const response = await fetch(`${window.location.origin}/api/memory`);
if (!response.ok) {
console.error('Memory fetch failed with status:', response.status);
memories = [];
memoriesLoading = false;
buildCategoryChips();
renderMemoryList();
updateMemoryCount();
@@ -297,12 +502,14 @@ export async function loadMemories() {
memories = [];
}
memoriesLoading = false;
buildCategoryChips();
renderMemoryList();
updateMemoryCount();
} catch (error) {
console.error('Failed to load memories:', error);
memories = [];
memoriesLoading = false;
buildCategoryChips();
renderMemoryList();
updateMemoryCount();
@@ -313,13 +520,16 @@ export async function loadMemories() {
// ---- Bulk select mode ----
const _SELECT_BTN_DOT_SVG = '<svg class="memory-select-btn-icon" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="11" r="3" fill="currentColor" stroke="none"/></svg>';
const _SELECT_BTN_X_SVG = '<svg class="memory-select-btn-icon" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" style="vertical-align:-2px;margin-right:3px;"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
function enterSelectMode() {
selectMode = true;
selectedIds.clear();
const bulkBar = document.getElementById('memory-bulk-bar');
const selectBtn = document.getElementById('memory-select-btn');
if (bulkBar) bulkBar.classList.remove('hidden');
if (selectBtn) { selectBtn.classList.add('active'); selectBtn.textContent = 'Cancel'; }
if (selectBtn) { selectBtn.classList.add('active'); selectBtn.innerHTML = _SELECT_BTN_X_SVG + 'Cancel'; }
updateBulkCount();
renderMemoryList();
}
@@ -331,7 +541,7 @@ function exitSelectMode() {
const selectBtn = document.getElementById('memory-select-btn');
const selectAll = document.getElementById('memory-select-all');
if (bulkBar) bulkBar.classList.add('hidden');
if (selectBtn) { selectBtn.classList.remove('active'); selectBtn.textContent = 'Select'; }
if (selectBtn) { selectBtn.classList.remove('active'); selectBtn.innerHTML = _SELECT_BTN_DOT_SVG + 'Select'; }
if (selectAll) selectAll.checked = false;
renderMemoryList();
}
@@ -428,7 +638,7 @@ export async function tidyMemories() {
const data = await res.json();
if ((data.removed || 0) === 0) {
if (tidySpinner) tidySpinner.destroy();
if (tidyBtn) { tidyBtn.disabled = false; tidyBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:-1px;margin-right:2px;"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg> Tidy'; }
if (tidyBtn) { tidyBtn.disabled = false; tidyBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:-1px;margin-right:2px;color:var(--accent, var(--red));"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg> Tidy'; }
showToast('Already clean');
return;
}
@@ -471,7 +681,7 @@ export async function tidyMemories() {
tidyBtn.disabled = false;
tidyBtn.style.border = '';
tidyBtn.style.background = '';
tidyBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:-1px;margin-right:2px;"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg> Tidy';
tidyBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:-1px;margin-right:2px;color:var(--accent, var(--red));"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg> Tidy';
}
}
}
@@ -587,6 +797,15 @@ export function renderMemoryList() {
memoryList.innerHTML = '';
if (filtered.length === 0) {
const selectBtn = document.getElementById('memory-select-btn');
if (selectBtn) selectBtn.disabled = true;
if (selectMode) exitSelectMode();
if (memoriesLoading) {
const row = spinnerModule.createLoadingRow('Loading memories...', 14);
row.classList.add('memory-empty');
memoryList.replaceChildren(row);
return;
}
const searchTerm = document.getElementById('memory-search')?.value?.trim() || '';
const _smiley = '<span style="vertical-align:-3px;margin-left:6px;">' + uiModule.emptyStateIcon('smiley') + '</span>';
if (searchTerm || activeCategory !== 'all') {
@@ -606,6 +825,9 @@ export function renderMemoryList() {
return;
}
const selectBtn = document.getElementById('memory-select-btn');
if (selectBtn) selectBtn.disabled = false;
filtered.forEach(memory => {
const item = document.createElement('div');
item.className = 'memory-item';
@@ -701,7 +923,7 @@ export function renderMemoryList() {
menuBtn.title = 'Actions';
const dropdown = document.createElement('div');
dropdown.className = 'memory-item-dropdown';
dropdown.className = 'dropdown session-dropdown-menu memory-item-dropdown';
// Pin / Unpin — bookmark icon matches the chat-session "Favorite" SVG.
// Filled when pinned, outlined when not.
@@ -716,19 +938,19 @@ export function renderMemoryList() {
const editItem = document.createElement('div');
editItem.className = 'dropdown-item-compact';
editItem.textContent = '✎ Edit';
editItem.innerHTML = '<span class="dropdown-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></span><span>Edit</span>';
editItem.addEventListener('click', () => { dropdown.style.display = 'none'; startInlineEdit(item, memory); });
const deleteItem = document.createElement('div');
deleteItem.className = 'dropdown-item-compact memory-dropdown-delete';
deleteItem.textContent = '✕ Delete';
deleteItem.className = 'dropdown-item-compact dropdown-item-danger';
deleteItem.innerHTML = '<span class="dropdown-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/></svg></span><span>Delete</span>';
deleteItem.addEventListener('click', () => { dropdown.style.display = 'none'; deleteMemory(memory.id); });
// Select — enters bulk-select mode and pre-selects this memory. Same
// pattern as the email/documents/skills Select item.
const selectItem = document.createElement('div');
selectItem.className = 'dropdown-item-compact';
selectItem.innerHTML = '<span class="dropdown-icon"><span style="font-size:16px;line-height:1;">●</span></span><span>Select</span>';
selectItem.innerHTML = `<span class="dropdown-icon">${SELECT_MENU_ICON}</span><span>Select</span>`;
selectItem.addEventListener('click', (e) => {
e.stopPropagation();
if (dropdown.parentNode) dropdown.remove();
@@ -743,32 +965,61 @@ export function renderMemoryList() {
// dismisses cleanly.
const cancelItem = document.createElement('div');
cancelItem.className = 'dropdown-item-compact dropdown-cancel-mobile';
cancelItem.textContent = '✕ Cancel';
cancelItem.innerHTML = '<span class="dropdown-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></span><span>Cancel</span>';
cancelItem.addEventListener('click', (e) => { e.stopPropagation(); if (dropdown.parentNode) dropdown.remove(); });
dropdown.appendChild(pinItem);
dropdown.appendChild(selectItem);
dropdown.appendChild(editItem);
dropdown.appendChild(deleteItem);
dropdown.appendChild(cancelItem);
const orderedMenuItems = orderActionMenuItems([
{ label: 'Edit', node: editItem },
{ label: 'Select', node: selectItem },
{ label: memory.pinned ? 'Unpin' : 'Pin', node: pinItem },
{ label: 'Delete', node: deleteItem },
{ label: 'Cancel', node: cancelItem },
]);
let addedActionDivider = false;
orderedMenuItems.forEach((entry, index) => {
if (!addedActionDivider && index > 0 && actionMenuRank(entry) >= 700) {
const divider = document.createElement('div');
divider.className = 'dropdown-divider';
dropdown.appendChild(divider);
addedActionDivider = true;
}
dropdown.appendChild(entry.node);
});
menuBtn.addEventListener('click', (e) => {
e.stopPropagation();
const openForButton = [...document.querySelectorAll('.memory-item-dropdown')]
.find(menu => menu._anchor === menuBtn);
if (openForButton) {
if (typeof openForButton._dismiss === 'function') openForButton._dismiss();
else openForButton.remove();
return;
}
// Close any other open dropdowns
document.querySelectorAll('.memory-item-dropdown').forEach(d => d.remove());
document.querySelectorAll('.memory-item-dropdown').forEach(d => {
if (typeof d._dismiss === 'function') d._dismiss();
else d.remove();
});
dropdown._anchor = menuBtn;
const rect = menuBtn.getBoundingClientRect();
dropdown.style.position = 'fixed';
dropdown.style.top = rect.bottom + 2 + 'px';
dropdown.style.top = rect.bottom + 4 + 'px';
dropdown.style.right = (window.innerWidth - rect.right) + 'px';
dropdown.style.left = 'auto';
dropdown.style.zIndex = '10001';
// Portaled to <body>, so it must outrank the Memory modal it belongs to.
// Tool modals get a monotonically increasing z-index from modalManager's
// bring-to-front counter, which climbs unbounded over a long session —
// once it passed the old hardcoded 10001 the menu rendered behind the
// panel (#4720). topPortalZ() derives the value from the live tool-window
// stack so the menu always sits just above, however high it has climbed.
dropdown.style.zIndex = String(topPortalZ());
dropdown.style.display = 'block';
document.body.appendChild(dropdown);
// Keep on-screen (mobile): flip above the button if it overflows the
// bottom, clamp the left edge, cap height as a last resort.
const dr = dropdown.getBoundingClientRect();
if (dr.bottom > window.innerHeight - 6) {
dropdown.style.top = Math.max(6, rect.top - dr.height - 2) + 'px';
dropdown.style.top = Math.max(6, rect.top - dr.height - 4) + 'px';
}
if (dr.left < 6) {
dropdown.style.right = Math.max(6, window.innerWidth - 6 - dr.width) + 'px';
@@ -848,8 +1099,17 @@ export function renderMemoryList() {
item.addEventListener('pointercancel', _lpCancel);
}
// Close dropdown on outside click
document.addEventListener('click', () => { if (dropdown.parentNode) dropdown.remove(); }, { once: false });
// Close dropdown on an outside click. Delay registration so the opening
// tap cannot immediately close its own menu.
const closeDropdown = () => {
document.removeEventListener('click', onOutsideClick, true);
if (dropdown.parentNode) dropdown.remove();
};
const onOutsideClick = (event) => {
if (!dropdown.contains(event.target) && !menuBtn.contains(event.target)) closeDropdown();
};
dropdown._dismiss = closeDropdown;
setTimeout(() => document.addEventListener('click', onOutsideClick, true), 0);
}
memoryList.appendChild(item);
@@ -954,6 +1214,11 @@ export function updateMemoryCount() {
const h2Count = document.getElementById('memory-count-h2');
const tabCount = document.getElementById('memory-count'); // optional (may be absent)
if (!h2Count && !tabCount) return;
if (memoriesLoading) {
if (h2Count) h2Count.textContent = 'loading...';
if (tabCount) tabCount.textContent = '...';
return;
}
const searchInput = document.getElementById('memory-search');
const searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : '';
@@ -977,6 +1242,7 @@ export function updateMemoryCount() {
export async function addNewMemory() {
const input = document.getElementById('new-memory-input');
const text = input.value.trim();
const category = _readNewMemoryCategory();
if (!text) {
showError('Memory text cannot be empty');
@@ -991,6 +1257,7 @@ export async function addNewMemory() {
},
body: JSON.stringify({
text: text,
category: category,
})
});
@@ -1127,7 +1394,7 @@ export async function extractMemory(sessionId) {
});
}
modal.classList.remove('hidden');
openMemoryModal('browse');
}
// ---- Export ----
@@ -1160,10 +1427,6 @@ async function handleImportFile(file) {
if (!file) return;
const sessionId = sessionModule?.getCurrentSessionId?.();
if (!sessionId) {
showError('Open a session first — import needs an AI model');
return;
}
const importBtn = document.getElementById('memory-import-btn');
const _origImportHtml = importBtn ? importBtn.innerHTML : '';
@@ -1180,7 +1443,9 @@ async function handleImportFile(file) {
try {
const formData = new FormData();
formData.append('file', file);
formData.append('session', sessionId);
if (sessionId) {
formData.append('session', sessionId);
}
const res = await fetch(`${window.location.origin}/api/memory/import`, {
method: 'POST',
@@ -1313,8 +1578,7 @@ async function handleImportFile(file) {
});
}
modal.classList.remove('hidden');
document.querySelector('.memory-tab[data-memory-tab="browse"]')?.click();
openMemoryModal('browse');
} catch (error) {
console.error('Import failed:', error);
showError('Import failed — ' + error.message);
@@ -1341,6 +1605,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Memory modal tabs
document.querySelectorAll('.memory-tab[data-memory-tab]').forEach(tab => {
tab.addEventListener('click', () => {
if (tab.hidden || tab.classList.contains('hidden')) return;
const target = tab.dataset.memoryTab;
document.querySelectorAll('.memory-tab').forEach(t => t.classList.toggle('active', t === tab));
document.querySelectorAll('.memory-tab-panel[data-memory-panel]').forEach(p => {
@@ -1348,7 +1613,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
// Lazy-load skills tab (cascade=true → play the domino-in entrance)
if (target === 'skills') {
import('./skills.js').then(m => { if (m.loadSkills) m.loadSkills(true); else if (m.default?.loadSkills) m.default.loadSkills(true); });
import('./skills.js?v=20260909kebabconsistency1').then(m => { if (m.loadSkills) m.loadSkills(true); else if (m.default?.loadSkills) m.default.loadSkills(true); });
}
});
});
@@ -1360,6 +1625,7 @@ document.addEventListener('DOMContentLoaded', () => {
renderMemoryList();
});
}
_initMemorySortPicker();
const tidyBtn = document.getElementById('memory-tidy-btn');
if (tidyBtn) tidyBtn.addEventListener('click', tidyMemories);
@@ -1413,7 +1679,9 @@ const memoryModule = {
buildCategoryChips,
tidyMemories,
importMemories,
exportMemories
exportMemories,
openMemoryModal,
openMemory
};
export default memoryModule;