From 217c7c3d6a18388363e5b56940ed3c4a72201b5b Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Tue, 28 Apr 2026 23:05:57 -0400 Subject: [PATCH] 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 --- cortex/static/app.js | 56 ++++++++++++++++++++++++++-------------- cortex/static/index.html | 5 +++- cortex/static/style.css | 41 ++++++++++++++++++++++------- 3 files changed, 73 insertions(+), 29 deletions(-) diff --git a/cortex/static/app.js b/cortex/static/app.js index a701a2e..a74ca2a 100644 --- a/cortex/static/app.js +++ b/cortex/static/app.js @@ -1253,18 +1253,32 @@ inputEl.addEventListener('input', syncHeight); // ── File editor ────────────────────────────────────────────── - const fileModal = document.getElementById('file-modal'); - const fileSidebar = document.getElementById('file-sidebar'); - const fileEditor = document.getElementById('file-editor'); - const filePreview = document.getElementById('file-preview'); - const fileRawBtn = document.getElementById('file-raw-btn'); - const filePreviewBtn = document.getElementById('file-preview-btn'); - const fileSaveBtn = document.getElementById('file-save-btn'); - const fileCloseBtn = document.getElementById('file-close-btn'); - const filesBtn = document.getElementById('files-btn'); + const fileModal = document.getElementById('file-modal'); + const fileSidebar = document.getElementById('file-sidebar'); + const fileEditorWrap = document.getElementById('file-editor-wrap'); + const filePreview = document.getElementById('file-preview'); + const fileRawBtn = document.getElementById('file-raw-btn'); + const filePreviewBtn = document.getElementById('file-preview-btn'); + const fileSaveBtn = document.getElementById('file-save-btn'); + const fileCloseBtn = document.getElementById('file-close-btn'); + const filesBtn = document.getElementById('files-btn'); - let fileMode = 'preview'; // 'edit' or 'preview' - let activeFileName = null; + let fileMode = 'preview'; // 'edit' or 'preview' + 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 const FILE_GROUPS = [ @@ -1346,17 +1360,19 @@ function setFileMode(mode) { fileMode = mode; if (mode === 'edit') { - fileEditor.classList.remove('hidden'); + fileEditorWrap.classList.remove('hidden'); filePreview.classList.remove('active'); fileRawBtn.classList.add('active'); filePreviewBtn.classList.remove('active'); + mdEditor.refresh(); + mdEditor.focus(); } else { - fileEditor.classList.add('hidden'); + fileEditorWrap.classList.add('hidden'); filePreview.classList.add('active'); fileRawBtn.classList.remove('active'); filePreviewBtn.classList.add('active'); if (typeof marked !== 'undefined') { - filePreview.innerHTML = marked.parse(fileEditor.value); + filePreview.innerHTML = marked.parse(mdEditor.getValue()); filePreview.querySelectorAll('a').forEach(a => { a.target = '_blank'; a.rel = 'noopener noreferrer'; }); @@ -1366,10 +1382,12 @@ async function loadFile(name) { setActiveFile(name); + initMdEditor(); 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(); - fileEditor.value = data.content; + mdEditor.setValue(data.content); + mdEditor.clearHistory(); setFileMode(fileMode); } @@ -1396,7 +1414,7 @@ const res = await fetch(`/files/${encodeURIComponent(activeFileName)}?${_fileParams}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content: fileEditor.value }), + body: JSON.stringify({ content: mdEditor.getValue() }), }); if (res.ok) { showToast('File saved', 'success'); @@ -1421,13 +1439,13 @@ const sessionSearchResults = document.getElementById('session-search-results'); function _showFileView() { - fileEditor.style.display = ''; + setFileMode(fileMode); filePreview.style.display = ''; sessionSearchResults.style.display = 'none'; } function _showSearchResults(html) { - fileEditor.style.display = 'none'; + fileEditorWrap.classList.add('hidden'); filePreview.style.display = 'none'; sessionSearchResults.style.display = ''; sessionSearchResults.innerHTML = html; diff --git a/cortex/static/index.html b/cortex/static/index.html index ec20dee..cbd55c3 100644 --- a/cortex/static/index.html +++ b/cortex/static/index.html @@ -20,6 +20,9 @@ })(); + + + @@ -140,7 +143,7 @@
- +
diff --git a/cortex/static/style.css b/cortex/static/style.css index c7195ed..a477963 100644 --- a/cortex/static/style.css +++ b/cortex/static/style.css @@ -1141,20 +1141,43 @@ flex-direction: column; } - #file-editor { + /* CodeMirror markdown editor */ + #file-editor-wrap { 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); color: var(--text); - border: none; - outline: none; - padding: 16px; font-family: 'Courier New', monospace; font-size: 0.85rem; line-height: 1.55; - resize: none; - display: block; + border: none; } + #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 { flex: 1; @@ -1164,8 +1187,8 @@ line-height: 1.6; } - #file-preview.active { display: block; } - #file-editor.hidden { display: none; } + #file-preview.active { display: block; } + #file-editor-wrap.hidden { display: none; } /* Talk activity badge on Sessions button */ #sessions-btn.talk-badge::after {