feat(app): add Electron desktop UI and evolve memory/retrieval layer
Introduce an Electron app with session browser, memory list, and usage views (vanilla JS + Vue scaffolding). On the data layer: add content_type and is_meta to messages for transcript control-plane filtering, introduce FTS5-backed memory recall with safe tokenization, support memory archival via forget() through the renamed --attune runtime, and expose anchors on memory records.
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
// Keyboard shortcut handling -- ported from the HTML mock.
|
||||
// Registers a single document-level keydown listener that dispatches
|
||||
// all navigation, mutation, and route-switching shortcuts.
|
||||
|
||||
import { state, IS_MAC } from './state.js';
|
||||
import {
|
||||
renderAll,
|
||||
renderMemoryList,
|
||||
renderSessionList,
|
||||
renderStatus,
|
||||
enterDetail,
|
||||
exitDetail,
|
||||
setRoute,
|
||||
setView,
|
||||
toggleSort,
|
||||
moveCursor,
|
||||
archive,
|
||||
restore,
|
||||
doUndo,
|
||||
navigateToSession
|
||||
} from './render.js';
|
||||
|
||||
export function initKeyboard() {
|
||||
document.addEventListener('keydown', e => {
|
||||
const inInput =
|
||||
document.activeElement.tagName === 'INPUT' ||
|
||||
document.activeElement.tagName === 'TEXTAREA';
|
||||
const mod = IS_MAC ? e.metaKey : e.ctrlKey;
|
||||
|
||||
// -- Route switching: Cmd+1/2/3/4 --
|
||||
if (mod && e.key === '1') { e.preventDefault(); setRoute('sessions'); return; }
|
||||
if (mod && e.key === '2') { e.preventDefault(); setView('active'); return; }
|
||||
if (mod && e.key === '3') { e.preventDefault(); setView('archived'); return; }
|
||||
if (mod && e.key === '4') { e.preventDefault(); setView('broken'); return; }
|
||||
|
||||
// -- Undo: Cmd+Z --
|
||||
if (mod && e.key.toLowerCase() === 'z' && !e.shiftKey) {
|
||||
if (state.lastArchiveSnapshot) { e.preventDefault(); doUndo(); }
|
||||
return;
|
||||
}
|
||||
|
||||
// -- When inside an input, only Escape is handled (to blur) --
|
||||
if (inInput) {
|
||||
if (e.key === 'Escape') e.target.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
// -- Search focus: / --
|
||||
if (e.key === '/') {
|
||||
e.preventDefault();
|
||||
const searchInput = document.getElementById('search');
|
||||
if (searchInput) { searchInput.focus(); searchInput.select(); }
|
||||
return;
|
||||
}
|
||||
|
||||
// -- Detail mode shortcuts --
|
||||
if (state.mode === 'detail') {
|
||||
if (e.key === 'Escape') { e.preventDefault(); exitDetail(); return; }
|
||||
if (state.route === 'memory' && (e.key === 'd' || e.key === 'D')) {
|
||||
e.preventDefault();
|
||||
const m = state.memories.find(x => x.id === state.detailId);
|
||||
if (m && m.archived) restore([m.id]);
|
||||
else if (m) archive([m.id]);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// -- List mode shortcuts --
|
||||
|
||||
// Navigation: j/k/arrows
|
||||
if (e.key === 'j' || e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (state.route === 'memory') moveCursor(1, e.shiftKey);
|
||||
return;
|
||||
}
|
||||
if (e.key === 'k' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (state.route === 'memory') moveCursor(-1, e.shiftKey);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open detail: Enter
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (state.route === 'memory' && state.cursorId) enterDetail(state.cursorId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Archive / Restore: d/D
|
||||
if (state.route === 'memory' && (e.key === 'd' || e.key === 'D')) {
|
||||
e.preventDefault();
|
||||
const ids = state.selection.size > 0
|
||||
? Array.from(state.selection)
|
||||
: state.cursorId ? [state.cursorId] : [];
|
||||
if (!ids.length) return;
|
||||
const m = state.memories.find(x => x.id === ids[0]);
|
||||
if (state.view === 'archived' || (m && m.archived)) restore(ids);
|
||||
else archive(ids);
|
||||
return;
|
||||
}
|
||||
|
||||
// Undo: u
|
||||
if (e.key === 'u') {
|
||||
e.preventDefault();
|
||||
if (state.lastArchiveSnapshot) doUndo();
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort toggle: s
|
||||
if (e.key === 's') { e.preventDefault(); toggleSort(); return; }
|
||||
|
||||
// Escape: clear selection or search
|
||||
if (e.key === 'Escape') {
|
||||
if (state.selection.size) {
|
||||
state.selection.clear();
|
||||
renderMemoryList();
|
||||
renderStatus();
|
||||
} else if (state.query) {
|
||||
state.query = '';
|
||||
const searchInput = document.getElementById('search');
|
||||
if (searchInput) searchInput.value = '';
|
||||
if (state.route === 'sessions') renderSessionList();
|
||||
else renderMemoryList();
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user