mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 11:10:18 +00:00
feat(memory): gate third-party notes drawer
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
||||
|
||||
### Added
|
||||
|
||||
- Add a read-only Third-party notes drawer to the Memory panel that lists configured note/knowledge MCP sources such as Joplin, Obsidian, Notion, and llm-wiki, while explicitly leaving automatic session recall unchanged.
|
||||
- Add a default-off, read-only Third-party notes drawer to the Memory panel that lists configured note/knowledge MCP sources such as Joplin, Obsidian, Notion, and llm-wiki when explicitly enabled with `webui_external_notes_sources` or `HERMES_WEBUI_EXTERNAL_NOTES_SOURCES=1`, while leaving automatic session recall unchanged.
|
||||
|
||||
## [v0.51.115] — 2026-05-22 — Release CM (stage-pr2731 — 1-PR — clarify prompt collapse/expand with chevron-icon polish)
|
||||
|
||||
|
||||
@@ -7878,6 +7878,7 @@ def _handle_memory_read(handler):
|
||||
"memory_mtime": mem_file.stat().st_mtime if mem_file.exists() else None,
|
||||
"user_mtime": user_file.stat().st_mtime if user_file.exists() else None,
|
||||
"soul_mtime": soul_file.stat().st_mtime if soul_file.exists() else None,
|
||||
"external_notes_enabled": _external_notes_sources_enabled(),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -11558,6 +11559,29 @@ def _handle_mcp_tools_list(handler):
|
||||
})
|
||||
|
||||
|
||||
def _webui_truthy(value) -> bool:
|
||||
return str(value or "").strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _external_notes_sources_enabled(config_data: dict | None = None) -> bool:
|
||||
"""Return whether the third-party notes drawer is explicitly enabled.
|
||||
|
||||
The Memory panel is a primary surface, so this power-user drawer stays
|
||||
default-off unless a deployment opts in through config or environment.
|
||||
"""
|
||||
env_value = os.getenv("HERMES_WEBUI_EXTERNAL_NOTES_SOURCES", "")
|
||||
if env_value:
|
||||
return _webui_truthy(env_value)
|
||||
cfg = config_data if isinstance(config_data, dict) else get_config()
|
||||
if not isinstance(cfg, dict):
|
||||
return False
|
||||
return _webui_truthy(
|
||||
cfg.get("webui_external_notes_sources")
|
||||
or cfg.get("external_notes_sources")
|
||||
or cfg.get("notes_sources_drawer")
|
||||
)
|
||||
|
||||
|
||||
_NOTES_SOURCE_SERVER_HINTS = {
|
||||
"joplin", "obsidian", "notion", "llm-wiki", "llmwiki", "wiki",
|
||||
"notes", "note", "knowledge", "kb", "readwise", "logseq",
|
||||
@@ -11707,6 +11731,16 @@ def _notes_sources_from_mcp_inventory(server_summaries: dict, tools: list[dict])
|
||||
def _handle_notes_sources_list(handler):
|
||||
"""List note/knowledge MCP sources for the WebUI Notes drawer."""
|
||||
cfg = get_config()
|
||||
if not _external_notes_sources_enabled(cfg):
|
||||
return j(handler, {
|
||||
"enabled": False,
|
||||
"sources": [],
|
||||
"source": "disabled",
|
||||
"inventory_scope": "disabled_by_default",
|
||||
"attach_supported": False,
|
||||
"automatic_recall_unchanged": True,
|
||||
"recent_ai_notes": [],
|
||||
})
|
||||
servers = cfg.get("mcp_servers", {})
|
||||
if not isinstance(servers, dict):
|
||||
servers = {}
|
||||
@@ -11721,6 +11755,7 @@ def _handle_notes_sources_list(handler):
|
||||
tools = _mcp_tools_from_registry(server_summaries)
|
||||
source = "tool_registry" if tools else "none"
|
||||
return j(handler, {
|
||||
"enabled": True,
|
||||
"sources": _notes_sources_from_mcp_inventory(server_summaries, tools),
|
||||
"source": source,
|
||||
"inventory_scope": "already_known_runtime_only",
|
||||
@@ -11936,6 +11971,8 @@ def _joplin_recent_ai_notes(*, limit: int = 6) -> list[dict]:
|
||||
|
||||
|
||||
def _handle_notes_search(handler, parsed):
|
||||
if not _external_notes_sources_enabled():
|
||||
return j(handler, {"source": "disabled", "results": [], "error": "External notes sources are disabled."}, status=404)
|
||||
query = parse_qs(parsed.query or "")
|
||||
source = str(query.get("source", ["joplin"])[0] or "joplin").strip().lower()
|
||||
q = str(query.get("q", [""])[0] or "").strip()
|
||||
@@ -11952,6 +11989,8 @@ def _handle_notes_search(handler, parsed):
|
||||
|
||||
|
||||
def _handle_notes_item(handler, parsed):
|
||||
if not _external_notes_sources_enabled():
|
||||
return j(handler, {"source": "disabled", "error": "External notes sources are disabled."}, status=404)
|
||||
query = parse_qs(parsed.query or "")
|
||||
source = str(query.get("source", ["joplin"])[0] or "joplin").strip().lower()
|
||||
note_id = str(query.get("id", [""])[0] or "").strip()
|
||||
|
||||
@@ -3837,6 +3837,7 @@ async function previewExternalNote(source, id) {
|
||||
}
|
||||
|
||||
async function openMemorySection(section, el) {
|
||||
if (section === 'external_notes' && _memoryData && !_memoryData.external_notes_enabled) return;
|
||||
_currentMemorySection = section;
|
||||
document.querySelectorAll('#memoryPanel .side-menu-item').forEach(e => e.classList.remove('active'));
|
||||
if (el) el.classList.add('active');
|
||||
@@ -5228,12 +5229,16 @@ async function loadMemory(force) {
|
||||
try {
|
||||
const data = await api('/api/memory');
|
||||
_memoryData = data;
|
||||
if (_currentMemorySection === 'external_notes' && !data.external_notes_enabled) {
|
||||
_currentMemorySection = null;
|
||||
}
|
||||
if (_currentMemorySection === 'external_notes') {
|
||||
await loadNotesSources(!!force);
|
||||
}
|
||||
if (panel) {
|
||||
panel.innerHTML = '';
|
||||
for (const s of MEMORY_SECTIONS) {
|
||||
if (s.key === 'external_notes' && !_memoryData.external_notes_enabled) continue;
|
||||
const el = document.createElement('button');
|
||||
el.type = 'button';
|
||||
el.className = 'side-menu-item';
|
||||
|
||||
@@ -60,6 +60,26 @@ def test_notes_sources_shows_configured_note_servers_without_tool_inventory():
|
||||
assert sources[0]["status"] == "configured"
|
||||
|
||||
|
||||
def test_external_notes_sources_drawer_is_default_off(monkeypatch):
|
||||
from api import routes
|
||||
|
||||
monkeypatch.delenv("HERMES_WEBUI_EXTERNAL_NOTES_SOURCES", raising=False)
|
||||
|
||||
assert routes._external_notes_sources_enabled({}) is False
|
||||
assert routes._external_notes_sources_enabled({"webui_external_notes_sources": False}) is False
|
||||
|
||||
|
||||
def test_external_notes_sources_drawer_can_be_enabled_by_config_or_env(monkeypatch):
|
||||
from api import routes
|
||||
|
||||
monkeypatch.delenv("HERMES_WEBUI_EXTERNAL_NOTES_SOURCES", raising=False)
|
||||
assert routes._external_notes_sources_enabled({"webui_external_notes_sources": True}) is True
|
||||
assert routes._external_notes_sources_enabled({"external_notes_sources": "yes"}) is True
|
||||
|
||||
monkeypatch.setenv("HERMES_WEBUI_EXTERNAL_NOTES_SOURCES", "1")
|
||||
assert routes._external_notes_sources_enabled({}) is True
|
||||
|
||||
|
||||
def test_joplin_search_notes_returns_safe_snippets(monkeypatch):
|
||||
from api import routes
|
||||
|
||||
@@ -162,6 +182,14 @@ def test_external_notes_ui_uses_minimal_lucide_icons_for_ai_recent_notes():
|
||||
assert "📚" not in notes_block
|
||||
|
||||
|
||||
def test_external_notes_menu_item_is_default_off_from_memory_payload():
|
||||
from pathlib import Path
|
||||
|
||||
panels = Path("static/panels.js").read_text(encoding="utf-8")
|
||||
assert "external_notes_enabled" in panels
|
||||
assert "if (s.key === 'external_notes' && !_memoryData.external_notes_enabled) continue;" in panels
|
||||
|
||||
|
||||
def test_external_notes_search_button_matches_minimal_dark_controls():
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
Reference in New Issue
Block a user