mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
COD-121 unified session list: persistent Session Manager modal (slice A, unit 3)
Adds a header-reachable Session Manager so the complete session list is available mid-session, not only on the welcome screen. - index.html: always-on header button (.btn-session-manager) + #sessionManagerModal (mirrors the Away Digest modal) with a search box + results list. - panels-ui.js: openSessionManager()/closeSessionManager()/_loadSessionManagerList() — loads GET /api/sessions/unified (limit 200), renders via the unit-2 _buildHistoryItem (rich items, mode/LIVE badges, open->select / closed->resume), debounced search wired to the endpoint's q= param, empty/error states. A modal-scoped Escape listener closes it even when focus is in the search input; backdrop click and item click also close it. - app.js: closeSessionManager() added to the global Escape chain. - styles.css: modal + list styling (items reuse .history-item). Verified on an isolated beta instance (Playwright): the header button opens the modal, it lists 200 sessions from /api/sessions/unified, a no-match query issues ?q= to the server and yields 0 items, clearing restores the list, clicking an item closes the modal and routes resume/select, and Escape closes it. Gates: tsc 0, lint 0, frontend-syntax + public-asset format clean, 17 tests pass.
This commit is contained in:
@@ -278,6 +278,100 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (modal) modal.classList.remove('active');
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Session Manager Modal (COD-121)
|
||||
// Persistent, header-reachable session list (GET /api/sessions/unified)
|
||||
// reachable mid-session, with a server-side search box. Reuses the
|
||||
// unit-2 history item renderer so clicking resumes/switches sessions.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
async openSessionManager() {
|
||||
const modal = document.getElementById('sessionManagerModal');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
// Escape closes the modal even while focus is in the search input. A
|
||||
// modal-scoped listener is robust regardless of the global Escape chain
|
||||
// (which runs other close handlers first and can short-circuit). Wire once.
|
||||
if (!this._sessionManagerEscWired) {
|
||||
this._sessionManagerEscWired = true;
|
||||
modal.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.closeSessionManager();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure cases are loaded so item subtitles can show "#caseName" labels.
|
||||
// Mirror loadHistorySessions(): prefer already-loaded this.cases.
|
||||
if (!Array.isArray(this.cases) || this.cases.length === 0) {
|
||||
try {
|
||||
const r = await fetch('/api/cases');
|
||||
const d = r.ok ? await r.json() : null;
|
||||
this.cases = d?.data || [];
|
||||
} catch {
|
||||
this.cases = this.cases || [];
|
||||
}
|
||||
}
|
||||
|
||||
const search = document.getElementById('sessionManagerSearch');
|
||||
if (search) {
|
||||
// Wire the debounced search input once (lazy — the element exists by
|
||||
// the time the modal is first opened, and mixin methods are bound).
|
||||
if (!this._sessionManagerSearchWired) {
|
||||
this._sessionManagerSearchWired = true;
|
||||
search.addEventListener('input', () => {
|
||||
const value = search.value.trim();
|
||||
this._debouncedCall('sessionManagerSearch', () => this._loadSessionManagerList(value), 200);
|
||||
});
|
||||
}
|
||||
search.value = '';
|
||||
search.focus();
|
||||
}
|
||||
await this._loadSessionManagerList('');
|
||||
},
|
||||
|
||||
closeSessionManager() {
|
||||
const modal = document.getElementById('sessionManagerModal');
|
||||
if (modal) modal.classList.remove('active');
|
||||
},
|
||||
|
||||
async _loadSessionManagerList(q = '') {
|
||||
const list = document.getElementById('sessionManagerList');
|
||||
if (!list) return;
|
||||
try {
|
||||
const url = '/api/sessions/unified?limit=200' + (q ? '&q=' + encodeURIComponent(q) : '');
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
const sessions = data.data?.sessions || [];
|
||||
list.replaceChildren();
|
||||
if (sessions.length === 0) {
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'empty-message';
|
||||
empty.textContent = q ? 'No sessions match your search' : 'No sessions found';
|
||||
list.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
for (const s of sessions) {
|
||||
const item = this._buildHistoryItem(s, this.cases, { showViewAll: false });
|
||||
// Close the modal when an item is clicked; the underlying
|
||||
// resume/select (registered on the item) still fires because this
|
||||
// is a capture-phase listener that does not stop propagation.
|
||||
item.addEventListener('click', () => this.closeSessionManager(), true);
|
||||
list.appendChild(item);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[_loadSessionManagerList]', err);
|
||||
list.replaceChildren();
|
||||
const errLine = document.createElement('p');
|
||||
errLine.className = 'empty-message';
|
||||
errLine.textContent = 'Failed to load sessions';
|
||||
list.appendChild(errLine);
|
||||
}
|
||||
},
|
||||
|
||||
setAwayDigestRange(range) {
|
||||
this.awayDigestRange = range;
|
||||
this._awayDigestLoadedSuccessfully = false;
|
||||
|
||||
Reference in New Issue
Block a user