diff --git a/api/routes.py b/api/routes.py
index 4174d3fb..f44ed8ac 100644
--- a/api/routes.py
+++ b/api/routes.py
@@ -10706,6 +10706,7 @@ def _handle_notes_sources_list(handler):
"inventory_scope": "already_known_runtime_only",
"attach_supported": False,
"automatic_recall_unchanged": True,
+ "recent_ai_notes": _joplin_recent_ai_notes(limit=6),
})
@@ -10827,6 +10828,93 @@ def _joplin_get_note(note_id: str) -> dict:
}
+_JOPLIN_AI_RECALL_NOTE_PRIORITY = [
+ ("CURRENT_CONTEXT_ID", "Current Context"),
+ ("OPEN_ISSUES_ID", "Open Issues"),
+ ("AGENT_MEMORY_ID", "Agent Memory"),
+ ("CONVENTIONS_ID", "Conventions / Preferences"),
+ ("INFRA_ID", "Infrastructure"),
+ ("SERVICES_ID", "Services"),
+]
+
+
+def _joplin_prefill_script_path() -> Path | None:
+ cfg = get_config()
+ path_value = cfg.get("prefill_messages_script") if isinstance(cfg, dict) else None
+ if not path_value:
+ return None
+ try:
+ return Path(str(path_value)).expanduser()
+ except Exception:
+ return None
+
+
+def _joplin_recall_note_refs(script_path: Path | None = None) -> list[dict]:
+ """Find stable Joplin note IDs referenced by the configured recall script.
+
+ This keeps the WebUI generic: it does not hard-code a user's note IDs, but
+ can still surface the notes that the configured AI prefill/recall script is
+ known to read for automatic context.
+ """
+ script_path = script_path or _joplin_prefill_script_path()
+ if not script_path or not script_path.exists() or not script_path.is_file():
+ return []
+ try:
+ text = script_path.read_text(encoding="utf-8", errors="replace")
+ except Exception:
+ return []
+ constants = {
+ match.group(1): match.group(2)
+ for match in re.finditer(r'(?m)^\s*([A-Z0-9_]+_ID)\s*=\s*["\']([A-Fa-f0-9]{16,64})["\']', text)
+ }
+ refs = []
+ seen = set()
+ for const_name, label in _JOPLIN_AI_RECALL_NOTE_PRIORITY:
+ note_id = constants.get(const_name)
+ if not note_id or note_id in seen:
+ continue
+ seen.add(note_id)
+ refs.append({
+ "id": note_id,
+ "label": label,
+ "constant": const_name,
+ "used_by": "ai_prefill",
+ "used_reason": "automatic_recall",
+ })
+ return refs
+
+
+def _joplin_recent_ai_notes(*, limit: int = 6) -> list[dict]:
+ """Return safe Joplin notes that the configured AI recall path recently uses."""
+ try:
+ limit = max(1, min(int(limit or 6), 20))
+ except Exception:
+ limit = 6
+ notes = []
+ for ref in _joplin_recall_note_refs()[:limit]:
+ try:
+ data = _joplin_api_get(f"/notes/{ref['id']}", {
+ "fields": "id,title,parent_id,updated_time,user_updated_time,created_time",
+ })
+ except Exception:
+ continue
+ note_id = _mcp_safe_display_text(data.get("id") or ref.get("id") or "", limit=64)
+ if not note_id:
+ continue
+ notes.append({
+ "id": note_id,
+ "title": _mcp_safe_display_text(data.get("title") or ref.get("label") or "Untitled", limit=180),
+ "label": _mcp_safe_display_text(ref.get("label") or "", limit=120),
+ "parent_id": _mcp_safe_display_text(data.get("parent_id") or "", limit=64),
+ "updated_time": data.get("user_updated_time") or data.get("updated_time"),
+ "created_time": data.get("created_time"),
+ "source": "joplin",
+ "used_by": ref.get("used_by") or "ai_prefill",
+ "used_reason": ref.get("used_reason") or "automatic_recall",
+ })
+ return notes
+
+
def _handle_notes_search(handler, parsed):
query = parse_qs(parsed.query or "")
source = str(query.get("source", ["joplin"])[0] or "joplin").strip().lower()
diff --git a/static/i18n.js b/static/i18n.js
index 78d7605f..011d66a6 100644
--- a/static/i18n.js
+++ b/static/i18n.js
@@ -1061,6 +1061,9 @@ const LOCALES = {
external_notes_no_tools: 'No read/search tools are currently visible for this source.',
external_notes_tool_count: (count) => `${count} note tool${Number(count)===1?'':'s'} available`,
external_notes_configured_hint: 'Tool names are expected from this configured source; live schemas will appear when the WebUI runtime exposes them.',
+ external_notes_recent_ai: 'Recently used by AI',
+ external_notes_auto: 'auto',
+ external_notes_recent_ai_reason: 'Automatic recall',
external_notes_search_placeholder: 'Search notes…',
external_notes_search_empty: 'Search a configured notes source to preview notes here.',
source_active: 'active',
diff --git a/static/panels.js b/static/panels.js
index d84a84cb..4510c1f8 100644
--- a/static/panels.js
+++ b/static/panels.js
@@ -3625,6 +3625,16 @@ function _renderExternalNotesSources() {
const selected = sources.find(src => (src.name || '').toLowerCase() === (_notesSelectedSource || '').toLowerCase()) || sources[0];
_notesSelectedSource = (selected && selected.name) || 'joplin';
const sourceOptions = sources.map(src => ``).join('');
+ const recentAiNotes = Array.isArray(data.recent_ai_notes) ? data.recent_ai_notes : [];
+ const recentAiHtml = recentAiNotes.length
+ ? `