236 lines
7.2 KiB
JavaScript
236 lines
7.2 KiB
JavaScript
// Entry point -- wires data, rendering, and event handlers together.
|
|||
|
|
// No exports; this is the bootstrap module.
|
||
|
|
|
||
|
|
import { loadInitialData } from './data.js';
|
||
|
|
import { state, IS_MAC } from './state.js';
|
||
|
|
import {
|
||
|
|
renderAll,
|
||
|
|
renderMemoryList,
|
||
|
|
renderSessionList,
|
||
|
|
setRoute,
|
||
|
|
setView,
|
||
|
|
setProject,
|
||
|
|
enterDetail,
|
||
|
|
archive,
|
||
|
|
restore,
|
||
|
|
setCursor,
|
||
|
|
navigateToSession,
|
||
|
|
switchView
|
||
|
|
} from './render.js';
|
||
|
|
import { initKeyboard } from './keys.js';
|
||
|
|
|
||
|
|
// -- Helpers ----------------------------------------------------------------
|
||
|
|
|
||
|
|
let searchDebounceTimer = null;
|
||
|
|
|
||
|
|
function debounce(fn, ms) {
|
||
|
|
return (...args) => {
|
||
|
|
clearTimeout(searchDebounceTimer);
|
||
|
|
searchDebounceTimer = setTimeout(() => fn(...args), ms);
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// -- Boot -------------------------------------------------------------------
|
||
|
|
|
||
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
||
|
|
await loadInitialData();
|
||
|
|
initKeyboard();
|
||
|
|
|
||
|
|
// -- Sidebar navigation (route switching + project filter) ----------------
|
||
|
|
|
||
|
|
const sidebar = document.querySelector('.sidebar');
|
||
|
|
if (sidebar) {
|
||
|
|
sidebar.addEventListener('click', e => {
|
||
|
|
const item = e.target.closest('.sidebar-item');
|
||
|
|
if (!item) return;
|
||
|
|
|
||
|
|
const route = item.dataset.route;
|
||
|
|
const project = item.dataset.project;
|
||
|
|
const view = item.dataset.view;
|
||
|
|
|
||
|
|
if (route) {
|
||
|
|
setRoute(route);
|
||
|
|
} else if (view) {
|
||
|
|
setView(view);
|
||
|
|
} else if (project !== undefined) {
|
||
|
|
setProject(project);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// -- Sidebar search (filter projects list) --------------------------------
|
||
|
|
|
||
|
|
const sidebarSearch = document.querySelector('.sidebar-search input');
|
||
|
|
if (sidebarSearch) {
|
||
|
|
sidebarSearch.addEventListener('input', e => {
|
||
|
|
state.projectSearch = e.target.value;
|
||
|
|
renderAll();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// -- Breadcrumb click (navigate back to list) -----------------------------
|
||
|
|
|
||
|
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||
|
|
if (breadcrumb) {
|
||
|
|
breadcrumb.addEventListener('click', e => {
|
||
|
|
const crumb = e.target.closest('[data-action]');
|
||
|
|
if (!crumb) return;
|
||
|
|
const action = crumb.dataset.action;
|
||
|
|
if (action === 'goto-sessions') { state.projectFilter = 'all'; setRoute('sessions'); }
|
||
|
|
else if (action === 'goto-memory') { state.projectFilter = 'all'; setView('active'); }
|
||
|
|
else if (action === 'goto-session-detail') { state.subagentId = null; state.subagentDescription = null; switchView(); renderAll(); }
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// -- Toolbar search with debounce -----------------------------------------
|
||
|
|
|
||
|
|
const searchInput = document.getElementById('search');
|
||
|
|
if (searchInput) {
|
||
|
|
const handleSearch = debounce(value => {
|
||
|
|
state.query = value;
|
||
|
|
if (state.route === 'sessions') renderSessionList();
|
||
|
|
else renderMemoryList();
|
||
|
|
}, 200);
|
||
|
|
|
||
|
|
searchInput.addEventListener('input', e => {
|
||
|
|
handleSearch(e.target.value);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// -- Sort toggle -----------------------------------------------------------
|
||
|
|
|
||
|
|
const sortToggle = document.querySelector('.sort-group');
|
||
|
|
if (sortToggle) {
|
||
|
|
sortToggle.addEventListener('click', () => {
|
||
|
|
state.sortDesc = !state.sortDesc;
|
||
|
|
sortToggle.classList.toggle('desc', state.sortDesc);
|
||
|
|
sortToggle.classList.toggle('asc', !state.sortDesc);
|
||
|
|
if (state.route === 'sessions') renderSessionList();
|
||
|
|
else renderMemoryList();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// -- Search messages toggle ------------------------------------------------
|
||
|
|
|
||
|
|
const searchMsgsToggle = document.querySelector('.filter-toggle');
|
||
|
|
if (searchMsgsToggle) {
|
||
|
|
searchMsgsToggle.addEventListener('click', () => {
|
||
|
|
state.includeMessageBodies = !state.includeMessageBodies;
|
||
|
|
searchMsgsToggle.classList.toggle('active', state.includeMessageBodies);
|
||
|
|
if (state.query) {
|
||
|
|
if (state.route === 'sessions') renderSessionList();
|
||
|
|
else renderMemoryList();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// -- #list click (memory rows: selection, actions, navigation) ------------
|
||
|
|
|
||
|
|
const list = document.getElementById('list');
|
||
|
|
if (list) {
|
||
|
|
list.addEventListener('click', e => {
|
||
|
|
// Action buttons (archive/restore)
|
||
|
|
const action = e.target.closest('.row-action');
|
||
|
|
if (action) {
|
||
|
|
e.stopPropagation();
|
||
|
|
const row = action.closest('.row');
|
||
|
|
const id = row?.dataset.id;
|
||
|
|
if (!id) return;
|
||
|
|
if (action.classList.contains('restore')) {
|
||
|
|
restore([id]);
|
||
|
|
} else {
|
||
|
|
archive([id]);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Checkbox toggling
|
||
|
|
const checkbox = e.target.closest('.row-checkbox');
|
||
|
|
if (checkbox) {
|
||
|
|
e.stopPropagation();
|
||
|
|
const row = checkbox.closest('.row');
|
||
|
|
const id = row?.dataset.id;
|
||
|
|
if (!id) return;
|
||
|
|
|
||
|
|
if (e.shiftKey && state.cursorId) {
|
||
|
|
// Range select between cursor and clicked
|
||
|
|
const rows = Array.from(list.querySelectorAll('.row'));
|
||
|
|
const ids = rows.map(r => r.dataset.id);
|
||
|
|
const fromIdx = ids.indexOf(state.cursorId);
|
||
|
|
const toIdx = ids.indexOf(id);
|
||
|
|
const [lo, hi] = fromIdx < toIdx ? [fromIdx, toIdx] : [toIdx, fromIdx];
|
||
|
|
for (let i = lo; i <= hi; i++) {
|
||
|
|
state.selection.add(ids[i]);
|
||
|
|
}
|
||
|
|
} else if (e.metaKey || e.ctrlKey) {
|
||
|
|
// Toggle single
|
||
|
|
if (state.selection.has(id)) state.selection.delete(id);
|
||
|
|
else state.selection.add(id);
|
||
|
|
} else {
|
||
|
|
// Simple toggle
|
||
|
|
if (state.selection.has(id)) state.selection.delete(id);
|
||
|
|
else state.selection.add(id);
|
||
|
|
}
|
||
|
|
setCursor(id);
|
||
|
|
renderMemoryList();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Row click (navigate cursor / open detail)
|
||
|
|
const row = e.target.closest('.row');
|
||
|
|
if (!row) return;
|
||
|
|
const id = row.dataset.id;
|
||
|
|
if (!id) return;
|
||
|
|
|
||
|
|
if (e.shiftKey && state.cursorId) {
|
||
|
|
// Shift-click: range select
|
||
|
|
const rows = Array.from(list.querySelectorAll('.row'));
|
||
|
|
const ids = rows.map(r => r.dataset.id);
|
||
|
|
const fromIdx = ids.indexOf(state.cursorId);
|
||
|
|
const toIdx = ids.indexOf(id);
|
||
|
|
const [lo, hi] = fromIdx < toIdx ? [fromIdx, toIdx] : [toIdx, fromIdx];
|
||
|
|
for (let i = lo; i <= hi; i++) {
|
||
|
|
state.selection.add(ids[i]);
|
||
|
|
}
|
||
|
|
renderMemoryList();
|
||
|
|
} else if ((IS_MAC ? e.metaKey : e.ctrlKey)) {
|
||
|
|
// Cmd/Ctrl-click: toggle selection
|
||
|
|
if (state.selection.has(id)) state.selection.delete(id);
|
||
|
|
else state.selection.add(id);
|
||
|
|
setCursor(id);
|
||
|
|
renderMemoryList();
|
||
|
|
} else {
|
||
|
|
// Plain click: move cursor
|
||
|
|
setCursor(id);
|
||
|
|
renderMemoryList();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// -- #list dblclick (open detail) -----------------------------------------
|
||
|
|
|
||
|
|
list.addEventListener('dblclick', e => {
|
||
|
|
const row = e.target.closest('.row');
|
||
|
|
if (!row) return;
|
||
|
|
const id = row.dataset.id;
|
||
|
|
if (id) enterDetail(id);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// -- #session-list click (session rows) ------------------------------------
|
||
|
|
|
||
|
|
const sessionList = document.getElementById('session-list');
|
||
|
|
if (sessionList) {
|
||
|
|
sessionList.addEventListener('click', e => {
|
||
|
|
const srow = e.target.closest('.srow');
|
||
|
|
if (!srow) return;
|
||
|
|
const id = srow.dataset.sessionId;
|
||
|
|
if (id) navigateToSession(id);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// -- Start on memory view -------------------------------------------------
|
||
|
|
|
||
|
|
setRoute('memory');
|
||
|
|
renderAll();
|
||
|
|
});
|