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:
@@ -1255,7 +1255,7 @@
|
|||||||
// ── 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');
|
||||||
@@ -1265,6 +1265,20 @@
|
|||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1165,7 +1188,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user