feat: CodeMirror markdown editor for identity/memory file editor

Replace plain textarea with CodeMirror 5 + markdown mode loaded from
jsDelivr CDN. Editor fills the modal body via flex layout, theme-aware
via CSS vars (cursor, selection, headings, bold/em/links/code all mapped
to Cortex dark/light palette). Lazy init on first file open; history
cleared per-file so undo doesn't bleed across files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-28 23:05:57 -04:00
parent 66cb197de0
commit 217c7c3d6a
3 changed files with 73 additions and 29 deletions

View File

@@ -1253,18 +1253,32 @@
inputEl.addEventListener('input', syncHeight); inputEl.addEventListener('input', syncHeight);
// ── File editor ────────────────────────────────────────────── // ── File editor ──────────────────────────────────────────────
const fileModal = document.getElementById('file-modal'); const fileModal = document.getElementById('file-modal');
const fileSidebar = document.getElementById('file-sidebar'); const fileSidebar = document.getElementById('file-sidebar');
const fileEditor = document.getElementById('file-editor'); const fileEditorWrap = document.getElementById('file-editor-wrap');
const filePreview = document.getElementById('file-preview'); const filePreview = document.getElementById('file-preview');
const fileRawBtn = document.getElementById('file-raw-btn'); const fileRawBtn = document.getElementById('file-raw-btn');
const filePreviewBtn = document.getElementById('file-preview-btn'); const filePreviewBtn = document.getElementById('file-preview-btn');
const fileSaveBtn = document.getElementById('file-save-btn'); const fileSaveBtn = document.getElementById('file-save-btn');
const fileCloseBtn = document.getElementById('file-close-btn'); const fileCloseBtn = document.getElementById('file-close-btn');
const filesBtn = document.getElementById('files-btn'); const filesBtn = document.getElementById('files-btn');
let fileMode = 'preview'; // 'edit' or 'preview' let fileMode = 'preview'; // 'edit' or 'preview'
let activeFileName = null; let activeFileName = null;
let mdEditor = null;
function initMdEditor() {
if (mdEditor) return;
mdEditor = CodeMirror(fileEditorWrap, {
mode: 'markdown',
lineWrapping: true,
lineNumbers: false,
autofocus: false,
tabSize: 2,
indentWithTabs: false,
extraKeys: { 'Ctrl-S': () => { fileSaveBtn.click(); return false; } },
});
}
// File groups — controls sidebar order and section labels // File groups — controls sidebar order and section labels
const FILE_GROUPS = [ const FILE_GROUPS = [
@@ -1346,17 +1360,19 @@
function setFileMode(mode) { function setFileMode(mode) {
fileMode = mode; fileMode = mode;
if (mode === 'edit') { if (mode === 'edit') {
fileEditor.classList.remove('hidden'); fileEditorWrap.classList.remove('hidden');
filePreview.classList.remove('active'); filePreview.classList.remove('active');
fileRawBtn.classList.add('active'); fileRawBtn.classList.add('active');
filePreviewBtn.classList.remove('active'); filePreviewBtn.classList.remove('active');
mdEditor.refresh();
mdEditor.focus();
} else { } else {
fileEditor.classList.add('hidden'); fileEditorWrap.classList.add('hidden');
filePreview.classList.add('active'); filePreview.classList.add('active');
fileRawBtn.classList.remove('active'); fileRawBtn.classList.remove('active');
filePreviewBtn.classList.add('active'); filePreviewBtn.classList.add('active');
if (typeof marked !== 'undefined') { if (typeof marked !== 'undefined') {
filePreview.innerHTML = marked.parse(fileEditor.value); filePreview.innerHTML = marked.parse(mdEditor.getValue());
filePreview.querySelectorAll('a').forEach(a => { filePreview.querySelectorAll('a').forEach(a => {
a.target = '_blank'; a.rel = 'noopener noreferrer'; a.target = '_blank'; a.rel = 'noopener noreferrer';
}); });
@@ -1366,10 +1382,12 @@
async function loadFile(name) { async function loadFile(name) {
setActiveFile(name); setActiveFile(name);
initMdEditor();
const res = await fetch(`/files/${encodeURIComponent(name)}?${_fileParams}`); const res = await fetch(`/files/${encodeURIComponent(name)}?${_fileParams}`);
if (!res.ok) { fileEditor.value = `Error loading ${name}`; return; } if (!res.ok) { mdEditor.setValue(`Error loading ${name}`); return; }
const data = await res.json(); const data = await res.json();
fileEditor.value = data.content; mdEditor.setValue(data.content);
mdEditor.clearHistory();
setFileMode(fileMode); setFileMode(fileMode);
} }
@@ -1396,7 +1414,7 @@
const res = await fetch(`/files/${encodeURIComponent(activeFileName)}?${_fileParams}`, { const res = await fetch(`/files/${encodeURIComponent(activeFileName)}?${_fileParams}`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: fileEditor.value }), body: JSON.stringify({ content: mdEditor.getValue() }),
}); });
if (res.ok) { if (res.ok) {
showToast('File saved', 'success'); showToast('File saved', 'success');
@@ -1421,13 +1439,13 @@
const sessionSearchResults = document.getElementById('session-search-results'); const sessionSearchResults = document.getElementById('session-search-results');
function _showFileView() { function _showFileView() {
fileEditor.style.display = ''; setFileMode(fileMode);
filePreview.style.display = ''; filePreview.style.display = '';
sessionSearchResults.style.display = 'none'; sessionSearchResults.style.display = 'none';
} }
function _showSearchResults(html) { function _showSearchResults(html) {
fileEditor.style.display = 'none'; fileEditorWrap.classList.add('hidden');
filePreview.style.display = 'none'; filePreview.style.display = 'none';
sessionSearchResults.style.display = ''; sessionSearchResults.style.display = '';
sessionSearchResults.innerHTML = html; sessionSearchResults.innerHTML = html;

View File

@@ -20,6 +20,9 @@
})(); })();
</script> </script>
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.17/lib/codemirror.min.css">
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.17/lib/codemirror.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.17/mode/markdown/markdown.min.js"></script>
<script src="/static/marked.min.js"></script> <script src="/static/marked.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/atom-one-dark.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/atom-one-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
@@ -140,7 +143,7 @@
</div> </div>
</div> </div>
<div id="file-modal-body"> <div id="file-modal-body">
<textarea id="file-editor" spellcheck="false"></textarea> <div id="file-editor-wrap"></div>
<div id="file-preview"></div> <div id="file-preview"></div>
<div id="session-search-results" style="display:none"></div> <div id="session-search-results" style="display:none"></div>
</div> </div>

View File

@@ -1141,20 +1141,43 @@
flex-direction: column; flex-direction: column;
} }
#file-editor { /* CodeMirror markdown editor */
#file-editor-wrap {
flex: 1; flex: 1;
width: 100%; min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
#file-editor-wrap.hidden { display: none; }
#file-editor-wrap .CodeMirror {
flex: 1;
height: 100%;
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
border: none;
outline: none;
padding: 16px;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-size: 0.85rem; font-size: 0.85rem;
line-height: 1.55; line-height: 1.55;
resize: none; border: none;
display: block;
} }
#file-editor-wrap .CodeMirror-scroll { padding: 12px 16px; }
#file-editor-wrap .CodeMirror-lines { padding: 0; }
#file-editor-wrap .CodeMirror-cursor { border-left-color: var(--accent); }
#file-editor-wrap .CodeMirror-selectedtext { background: var(--border) !important; }
#file-editor-wrap .CodeMirror-selected { background: var(--border) !important; }
#file-editor-wrap .CodeMirror-focused .CodeMirror-selected { background: var(--border) !important; }
/* Markdown token colours */
#file-editor-wrap .cm-header { color: var(--accent); font-weight: 600; }
#file-editor-wrap .cm-strong { color: var(--text); font-weight: 700; }
#file-editor-wrap .cm-em { color: var(--muted); font-style: italic; }
#file-editor-wrap .cm-link { color: #a78bfa; }
#file-editor-wrap .cm-url { color: var(--muted); }
#file-editor-wrap .cm-comment { color: var(--muted); }
#file-editor-wrap .cm-quote { color: var(--muted); font-style: italic; }
#file-editor-wrap .cm-code { color: var(--muted); background: var(--surface); }
#file-editor-wrap .cm-hr { color: var(--border); }
#file-preview { #file-preview {
flex: 1; flex: 1;
@@ -1164,8 +1187,8 @@
line-height: 1.6; line-height: 1.6;
} }
#file-preview.active { display: block; } #file-preview.active { display: block; }
#file-editor.hidden { display: none; } #file-editor-wrap.hidden { display: none; }
/* Talk activity badge on Sessions button */ /* Talk activity badge on Sessions button */
#sessions-btn.talk-badge::after { #sessions-btn.talk-badge::after {