Merge pull request #5052 from nesquena/release/stage-5050-memhover

Release: memory-section hover path titles (#5050, fixes #5045)
This commit is contained in:
nesquena-hermes 2026-06-27 03:57:19 -07:00 committed by GitHub
commit db15233c4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 106 additions and 2 deletions

View file

@ -5,6 +5,8 @@
### Fixed
- **The Memory panel's section list now shows each section's on-disk path on hover.** #5025 surfaced the backing file path in the selected memory detail pane; this extends that to the left-rail section buttons (Memory, User, Soul, Project context) as a hover tooltip (`title`), reusing the same path source so you can see where each section lives without selecting it first. Thanks @rodboev. (#5050, fixes #5045)
- **Gateway approval cards can be answered even after the live stream pointer is lost.** When you approve/deny a tool-use request that came through the gateway, `/api/approval/respond` walked back through the session's `active_stream_id` to find the run — so if that pointer was gone (reconnect, background tab, stream ended), responding failed with `gateway_run_unavailable` even though the mirrored approval card still carried the originating gateway run info. The respond handler now relays a mirrored gateway approval using the approval's own carried origin (scoped to the same session + approval id), so the card stays actionable across stream loss. The normal stream-alive path is unchanged, auth/CSRF gates are untouched, and a genuinely origin-less approval still returns `gateway_run_unavailable`. Thanks @rodboev. (#5041, fixes #5000)
- **Installing an extension now shows its post-install next steps.** Some extensions need a follow-up step after install (run a setup command, restart, configure a key). The Settings → Extensions gallery now renders that guidance from the extension's own registry metadata — generic `post_install` text and lifecycle requirements, with an optional docs link — right on the card, plus a "see the card for next steps" install toast. The guidance is registry-driven (no vendor special-casing), every value is HTML-escaped, any docs URL is validated to safe http(s) before it's linked, and the note degrades cleanly when an extension declares no post-install steps. Thanks @franksong2702. (#4964, fixes #4959)

View file

@ -4629,8 +4629,8 @@ function _memorySectionPath(key) {
if (key === 'user') return _memoryData.user_path || '';
if (key === 'soul') return _memoryData.soul_path || '';
if (key === 'project_context') return _memoryData.project_context_path || '';
// Intentional default: the primary "memory" section remains the fallback.
return _memoryData.memory_path || '';
if (key === 'memory') return _memoryData.memory_path || '';
return '';
}
function _setMemoryHeaderButtons(mode) {
@ -6483,6 +6483,8 @@ async function loadMemory(force) {
el.className = 'side-menu-item';
if (_currentMemorySection === s.key) el.classList.add('active');
el.innerHTML = `${li(s.iconKey,16)}<span>${esc(_memorySectionLabel(s))}</span>`;
const sectionPath = _memorySectionPath(s.key);
if (sectionPath) el.title = sectionPath;
el.onclick = () => openMemorySection(s.key, el);
panel.appendChild(el);
}

View file

@ -141,6 +141,8 @@ def test_memory_panel_references_all_memory_path_fields():
assert "_memoryData.user_path" in panels
assert "_memoryData.soul_path" in panels
assert "_memoryData.project_context_path" in panels
assert "const sectionPath = _memorySectionPath(s.key)" in panels
assert "if (sectionPath) el.title = sectionPath" in panels
def _memory_render_blocks():
@ -222,6 +224,90 @@ console.log(JSON.stringify({memoryHtml, userHtml, soulHtml, projectHtml, memoryM
return json.loads(completed.stdout)
def _memory_button_render_blocks():
panels = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8")
sections_start = panels.index("const MEMORY_SECTIONS = [")
sections_end = panels.index("];", sections_start) + 2
helper_start = panels.index("function _memorySectionPath(key)")
helper_end = panels.index("function _setMemoryHeaderButtons", helper_start)
loop_start = panels.index("for (const s of MEMORY_SECTIONS) {")
loop_end = panels.index(" if (_currentMemorySection && _memoryMode !== 'edit') {", loop_start)
return (
panels[sections_start:sections_end],
panels[helper_start:helper_end],
panels[loop_start:loop_end].rsplit(" }", 1)[0],
)
def _run_memory_button_harness():
sections_block, helper_block, loop_block = _memory_button_render_blocks()
script = (
"const sectionsBlock = "
+ json.dumps(sections_block)
+ ";\nconst helperBlock = "
+ json.dumps(helper_block)
+ ";\nconst loopBlock = "
+ json.dumps(loop_block)
+ ";\n"
+ r"""
let _memoryData = {
memory_path: 'C:/Users/Rod/.hermes/memories/MEMORY.md',
user_path: 'C:/Users/Rod/.hermes/memories/USER.md',
soul_path: 'C:/Users/Rod/.hermes/SOUL.md',
project_context_path: 'D:/Repos/hermes-webui/AGENTS.md',
external_notes_enabled: true,
};
let _currentMemorySection = 'memory';
const nodes = {
memoryPanel: {
innerHTML: '',
appended: [],
appendChild(node) { this.appended.push(node); },
},
};
const document = {
createElement() {
return {
title: '',
type: '',
className: '',
innerHTML: '',
onclick: null,
classList: { add() {} },
setAttribute(name, value) { this[name] = value; },
};
},
};
function _memorySectionLabel(meta) { return meta.key; }
function li() { return ''; }
function esc(value) { return String(value); }
function openMemorySection() {}
eval(
sectionsBlock +
"\n" +
helperBlock +
"\nfunction renderButtons() {\nconst panel = nodes.memoryPanel;\n" +
loopBlock +
"\n}\nrenderButtons();"
);
const buttons = nodes.memoryPanel.appended.map(btn => ({
label: btn.innerHTML.replace(/<[^>]+>/g, ''),
title: btn.title || '',
}));
console.log(JSON.stringify(buttons));
"""
)
completed = subprocess.run(
[NODE, "-e", script],
capture_output=True,
text=True,
encoding="utf-8",
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.
@ -244,6 +330,20 @@ def test_memory_detail_renders_path_for_non_project_sections():
assert "CLAUDE.md present, shadowed by AGENTS.md" in rendered["projectHtml"]
def test_memory_section_list_renders_hover_path_titles():
"""Base-fails/head-passes regression for issue #5045."""
if NODE is None:
pytest.skip("node not on PATH")
rendered = {item["label"]: item["title"] for item in _run_memory_button_harness()}
assert rendered["memory"] == "C:/Users/Rod/.hermes/memories/MEMORY.md"
assert rendered["user"] == "C:/Users/Rod/.hermes/memories/USER.md"
assert rendered["soul"] == "C:/Users/Rod/.hermes/SOUL.md"
assert rendered["project_context"] == "D:/Repos/hermes-webui/AGENTS.md"
assert rendered["external_notes"] == ""
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