diff --git a/static/panels.js b/static/panels.js index 30d19c2b..4a76ae6e 100644 --- a/static/panels.js +++ b/static/panels.js @@ -4624,6 +4624,14 @@ function _memorySectionMtime(key) { return _memoryData.memory_mtime || 0; } +function _memorySectionPath(key) { + if (!_memoryData) return ''; + if (key === 'user') return _memoryData.user_path || ''; + if (key === 'soul') return _memoryData.soul_path || ''; + if (key === 'project_context') return _memoryData.project_context_path || ''; + return _memoryData.memory_path || ''; +} + function _setMemoryHeaderButtons(mode) { const header = $('mainMemory') && $('mainMemory').querySelector('.main-view-header'); const show = b => b && (b.style.display = ''); @@ -4727,8 +4735,10 @@ function _renderMemoryDetail(section) { const mtime = _memorySectionMtime(section); const mtimeStr = mtime ? new Date(mtime * 1000).toLocaleString() : ''; const mtimeHtml = mtimeStr ? `
${esc(mtimeStr)}
` : ''; - const path = section === 'project_context' && _memoryData ? (_memoryData.project_context_path || '') : ''; - const fileName = section === 'project_context' && _memoryData ? (_memoryData.project_context_name || (path.split(/[\\/]/).pop() || '')) : ''; + const path = _memorySectionPath(section); + const fileName = section === 'project_context' && _memoryData + ? (_memoryData.project_context_name || (path.split(/[\\/]/).pop() || '')) + : (path.split(/[\\/]/).pop() || ''); const pathHtml = path ? `
${esc(fileName)} · ${esc(path)}
` : ''; const shadowed = section === 'project_context' && _memoryData && Array.isArray(_memoryData.project_context_shadowed) ? _memoryData.project_context_shadowed diff --git a/tests/test_3866_project_context_memory_section.py b/tests/test_3866_project_context_memory_section.py index 83b4e6a0..b9375077 100644 --- a/tests/test_3866_project_context_memory_section.py +++ b/tests/test_3866_project_context_memory_section.py @@ -1,5 +1,7 @@ import json import pathlib +import shutil +import subprocess from types import SimpleNamespace from urllib.parse import urlencode @@ -8,6 +10,7 @@ import api.routes as routes REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve() +NODE = shutil.which("node") def project_context_for(workspace): @@ -129,6 +132,108 @@ def test_memory_panel_defines_read_only_project_context_section(): assert "/api/memory?session_id=" in panels +def test_memory_panel_references_all_memory_path_fields(): + panels = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8") + + assert "function _memorySectionPath(key)" in panels + assert "_memoryData.memory_path" in panels + assert "_memoryData.user_path" in panels + assert "_memoryData.soul_path" in panels + assert "_memoryData.project_context_path" in panels + + +def _memory_render_blocks(): + panels = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8") + helper_start = panels.index("function _memorySectionContent(key)") + helper_end = panels.index("function _setMemoryHeaderButtons", helper_start) + render_start = panels.index("function _renderMemoryDetail(section)") + render_end = panels.index("function _renderMemoryEdit", render_start) + return panels[helper_start:helper_end], panels[render_start:render_end] + + +def _run_memory_render_harness(): + helper_block, render_block = _memory_render_blocks() + script = ( + "const helperBlock = " + + json.dumps(helper_block) + + ";\nconst renderBlock = " + + json.dumps(render_block) + + ";\n" + + r""" +let _memoryData = { + memory: 'Primary memory body', + memory_path: 'C:/Users/Rod/.hermes/memories/MEMORY.md', + memory_mtime: 1712345678, + user: 'User memory body', + user_path: 'C:/Users/Rod/.hermes/memories/USER.md', + user_mtime: 1712345678, + soul: 'Soul memory body', + soul_path: 'C:/Users/Rod/.hermes/SOUL.md', + soul_mtime: 1712345678, + project_context: 'Project context body', + project_context_path: 'D:/Repos/hermes-webui/AGENTS.md', + project_context_name: 'AGENTS.md', + project_context_mtime: 1712345678, + project_context_shadowed: [{name: 'CLAUDE.md', shadowed_by: 'AGENTS.md'}], +}; +let _memoryMode = ''; +const nodes = { + memoryDetailTitle: {textContent: '', style: {}}, + memoryDetailBody: {innerHTML: '', style: {}}, + memoryDetailEmpty: {style: {}}, +}; +function $(id) { return nodes[id] || null; } +function _memorySectionMeta(section) { + return {key: section, label: section, emptyKey: section + '_empty'}; +} +function _memorySectionLabel(meta) { return meta.label; } +function _memorySectionEmpty(meta) { return meta.emptyKey; } +function _setMemoryHeaderButtons() {} +function renderMd(content) { return 'rendered:' + content; } +function esc(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} +eval(helperBlock + "\n" + renderBlock); +_renderMemoryDetail('memory'); +const memoryHtml = nodes.memoryDetailBody.innerHTML; +_renderMemoryDetail('project_context'); +const projectHtml = nodes.memoryDetailBody.innerHTML; +console.log(JSON.stringify({memoryHtml, projectHtml, memoryMode: _memoryMode})); +""" + ) + completed = subprocess.run( + [NODE, "-e", script], + capture_output=True, + text=True, + timeout=30, + check=True, + ) + return json.loads(completed.stdout) + + +def test_memory_detail_renders_path_for_non_project_sections(): + """Base-fails/head-passes regression for issue #4999. + + On base, `_renderMemoryDetail('memory')` ignores `memory_path`, so the + rendered header omits the path row entirely. On head, the same render must + show `MEMORY.md · ` using the existing pinned header row pattern. + """ + if NODE is None: + return + + rendered = _run_memory_render_harness() + + assert "MEMORY.md" in rendered["memoryHtml"] + assert "C:/Users/Rod/.hermes/memories/MEMORY.md" in rendered["memoryHtml"] + assert "AGENTS.md · D:/Repos/hermes-webui/AGENTS.md" in rendered["projectHtml"] + assert "CLAUDE.md present, shadowed by AGENTS.md" in rendered["projectHtml"] + + def test_blank_session_workspace_does_not_resolve_to_server_cwd(monkeypatch): # Regression for the empty-workspace guard: a session with a blank workspace # (freshly-created/draft sessions) must return None rather than letting