From 5b2ff6e712dead108ce7ac586b149f0d569913e6 Mon Sep 17 00:00:00 2001
From: Christian Wendler
Date: Wed, 8 Apr 2026 11:01:31 +0000
Subject: [PATCH 1/3] fix: WebUI form consistency, task visibility, markdown
rendering
- Fix select/input height mismatch in New Entry form (explicit 38px)
- Convert Project + Agent fields to datalist with autocomplete from API
- Add /api/tags endpoint + tags datalist for autocomplete
- Move Active Tasks from invisible page-bottom to sidebar (always visible)
- Render detail body as formatted Markdown instead of raw monospace
- Simplify layout from 3-column to 2-column (tasks now in sidebar)
Co-Authored-By: Claude Opus 4.6
---
palaia/web/routes/status.py | 17 +++++++++
palaia/web/static/app.js | 51 ++++++++++++++++++++++++++-
palaia/web/static/index.html | 33 ++++++++++--------
palaia/web/static/style.css | 67 +++++++++++++++++++++++++-----------
4 files changed, 132 insertions(+), 36 deletions(-)
diff --git a/palaia/web/routes/status.py b/palaia/web/routes/status.py
index b03bb1d..fe8abb1 100644
--- a/palaia/web/routes/status.py
+++ b/palaia/web/routes/status.py
@@ -92,6 +92,23 @@ def list_agents(request: Request) -> dict:
return {"agents": sorted(agents)}
+@router.get("/tags")
+def list_tags(request: Request) -> dict:
+ """List distinct tags found in entries."""
+ from palaia.store import Store
+
+ root = request.app.state.palaia_root
+ store = Store(root)
+ store.recover()
+ tags: set[str] = set()
+ for meta, _body, _tier in store.all_entries_unfiltered(include_cold=True):
+ for tag in meta.get("tags") or []:
+ tag = tag.strip()
+ if tag and tag != "auto-capture":
+ tags.add(tag)
+ return {"tags": sorted(tags)}
+
+
@router.get("/doctor")
def run_doctor(request: Request) -> dict:
"""Run palaia doctor and return results for the UI banner.
diff --git a/palaia/web/static/app.js b/palaia/web/static/app.js
index 68ea577..694f544 100644
--- a/palaia/web/static/app.js
+++ b/palaia/web/static/app.js
@@ -80,6 +80,39 @@
toast._timer = setTimeout(() => { t.className = "toast"; }, 3000);
}
+ // ── Simple Markdown → HTML ────────────────────────────────────────────────
+ function renderMarkdown(text) {
+ if (!text) return "";
+ // Escape HTML first (security)
+ let html = esc(text);
+ // Code blocks (``` ... ```)
+ html = html.replace(/```[\s\S]*?```/g, (m) => {
+ const code = m.slice(3, -3).replace(/^\w*\n/, "");
+ return "" + code + "
";
+ });
+ // Inline code
+ html = html.replace(/`([^`]+)`/g, "$1");
+ // Headings
+ html = html.replace(/^### (.+)$/gm, "$1
");
+ html = html.replace(/^## (.+)$/gm, "$1
");
+ html = html.replace(/^# (.+)$/gm, "$1
");
+ // Bold + italic
+ html = html.replace(/\*\*(.+?)\*\*/g, "$1");
+ html = html.replace(/\*(.+?)\*/g, "$1");
+ // Blockquotes
+ html = html.replace(/^> (.+)$/gm, "$1
");
+ // Unordered lists
+ html = html.replace(/^- (.+)$/gm, "$1");
+ html = html.replace(/((?:.*<\/li>\n?)+)/g, "");
+ // Ordered lists
+ html = html.replace(/^\d+\. (.+)$/gm, "$1");
+ // Paragraphs (double newline)
+ html = html.replace(/\n\n/g, "
");
+ // Single newlines within paragraphs →
+ html = html.replace(/\n/g, "
");
+ return "
" + html + "
";
+ }
+
// ── Date formatting ──────────────────────────────────────────────────────
function fmtDate(iso) {
if (!iso) return "—";
@@ -103,8 +136,10 @@
try {
const d = await apiGet("/api/projects");
const sel = $("filter-project");
+ const dl = $("project-options");
for (const name of Object.keys(d.projects || {})) {
sel.appendChild(el("option", { value: name }, name));
+ dl.appendChild(el("option", { value: name }));
}
} catch (e) { /* non-fatal */ }
}
@@ -113,8 +148,20 @@
try {
const d = await apiGet("/api/agents");
const sel = $("filter-agent");
+ const dl = $("agent-options");
for (const name of d.agents || []) {
sel.appendChild(el("option", { value: name }, name));
+ dl.appendChild(el("option", { value: name }));
+ }
+ } catch (e) { /* non-fatal */ }
+ }
+
+ async function loadTags() {
+ try {
+ const d = await apiGet("/api/tags");
+ const dl = $("tag-options");
+ for (const tag of d.tags || []) {
+ dl.appendChild(el("option", { value: tag }));
}
} catch (e) { /* non-fatal */ }
}
@@ -347,7 +394,8 @@
),
);
- const body = el("pre", { class: "detail-body" }, d.content || "");
+ const body = el("div", { class: "detail-body" });
+ body.innerHTML = renderMarkdown(d.content || "");
return el("div", { id: "inline-detail-" + id, class: "inline-detail" }, toolbar, dl, body);
}
@@ -598,6 +646,7 @@
loadStatus();
loadProjects();
loadAgents();
+ loadTags();
loadDoctor();
loadEntries();
loadTasks();
diff --git a/palaia/web/static/index.html b/palaia/web/static/index.html
index be7b42a..6c66a90 100644
--- a/palaia/web/static/index.html
+++ b/palaia/web/static/index.html
@@ -39,6 +39,9 @@
@@ -115,18 +130,6 @@
-
-
-
@@ -168,18 +171,18 @@ New Entry
-
+
diff --git a/palaia/web/static/style.css b/palaia/web/static/style.css
index 2386adc..136fd13 100644
--- a/palaia/web/static/style.css
+++ b/palaia/web/static/style.css
@@ -144,17 +144,13 @@ body {
.doctor-warn .doctor-label { color: var(--yellow); }
.doctor-error .doctor-label { color: var(--red); }
-/* ── 3-column layout ─────────────────────────────────────────────────── */
+/* ── 2-column layout ─────────────────────────────────────────────────── */
.layout {
display: grid;
- grid-template-columns: 240px 1fr 300px;
+ grid-template-columns: 260px 1fr;
gap: 20px;
align-items: start;
}
-@media (max-width: 1200px) {
- .layout { grid-template-columns: 220px 1fr; }
- .tasks-panel { grid-column: 1 / -1; }
-}
@media (max-width: 800px) {
.layout { grid-template-columns: 1fr; }
}
@@ -379,25 +375,54 @@ body {
background: var(--bg);
padding: 14px;
border-radius: var(--radius);
- font-family: var(--mono);
- font-size: 0.82em;
- white-space: pre-wrap;
+ font-size: 0.85em;
word-wrap: break-word;
- line-height: 1.5;
+ line-height: 1.6;
max-height: 500px;
overflow-y: auto;
}
-
-/* ── Tasks panel ─────────────────────────────────────────────────────── */
-.tasks-panel {
+.detail-body h1, .detail-body h2, .detail-body h3 {
+ color: var(--text-bright);
+ margin: 12px 0 6px;
+ line-height: 1.3;
+}
+.detail-body h1 { font-size: 1.2em; }
+.detail-body h2 { font-size: 1.05em; }
+.detail-body h3 { font-size: 0.95em; }
+.detail-body p { margin: 6px 0; }
+.detail-body ul, .detail-body ol { margin: 6px 0; padding-left: 20px; }
+.detail-body li { margin: 3px 0; }
+.detail-body code {
background: var(--bg-card);
- border: 1px solid var(--border);
+ padding: 1px 5px;
+ border-radius: 3px;
+ font-family: var(--mono);
+ font-size: 0.9em;
+}
+.detail-body pre {
+ background: var(--bg-card);
+ padding: 10px;
border-radius: var(--radius);
- padding: 16px;
- position: sticky;
- top: 16px;
- max-height: calc(100vh - 32px);
- overflow-y: auto;
+ font-family: var(--mono);
+ font-size: 0.88em;
+ white-space: pre-wrap;
+ margin: 8px 0;
+ border: 1px solid var(--border);
+}
+.detail-body strong { color: var(--text-bright); }
+.detail-body em { color: var(--text); font-style: italic; }
+.detail-body blockquote {
+ border-left: 3px solid var(--accent-dim);
+ padding-left: 12px;
+ color: var(--text-dim);
+ margin: 8px 0;
+}
+
+/* ── Tasks (in sidebar) ──────────────────────────────────────────────── */
+.sidebar-tasks {
+ border-top: 1px solid var(--border);
+ margin-top: 14px;
+ padding-top: 14px;
}
.tasks-header {
display: flex;
@@ -529,8 +554,10 @@ body {
color: var(--text);
font-family: var(--font);
font-size: 0.9em;
+ height: 38px;
+ line-height: 1.4;
}
-#entry-form textarea { font-family: var(--mono); resize: vertical; min-height: 120px; }
+#entry-form textarea { font-family: var(--mono); resize: vertical; min-height: 120px; height: auto; }
#entry-form textarea:focus,
#entry-form input:focus,
#entry-form select:focus { border-color: var(--accent); outline: none; }
From 05c20f9ad6e2c1cb2a1b44caab5ae358e0d02817 Mon Sep 17 00:00:00 2001
From: Christian Wendler
Date: Thu, 9 Apr 2026 00:28:02 +0000
Subject: [PATCH 2/3] feat: source tags (webui/cli/auto-capture) for entry
provenance
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Entries now carry a source tag indicating how they were created:
- webui: created via WebUI (human, 1.3× boost)
- cli: created via palaia add/write (human, 1.3× boost)
- auto-capture: passively captured (no boost)
- no source tag: agent-created via MCP/tool (no boost)
Updates badge display, detail panel, and OpenClaw recall ranking.
Co-Authored-By: Claude Opus 4.6
---
packages/openclaw-plugin/src/hooks/recall.ts | 7 ++--
palaia/cli.py | 7 +++-
palaia/web/routes/entries.py | 42 +++++++++++++++-----
palaia/web/routes/search.py | 10 +++--
palaia/web/routes/status.py | 2 +-
palaia/web/static/app.js | 22 +++++++---
palaia/web/static/style.css | 1 +
7 files changed, 65 insertions(+), 26 deletions(-)
diff --git a/packages/openclaw-plugin/src/hooks/recall.ts b/packages/openclaw-plugin/src/hooks/recall.ts
index 148977a..44046c1 100644
--- a/packages/openclaw-plugin/src/hooks/recall.ts
+++ b/packages/openclaw-plugin/src/hooks/recall.ts
@@ -374,10 +374,11 @@ export function rerankByTypeWeight(
const type = r.type || "memory";
const weight = weights[type] ?? 1.0;
const recency = calcRecencyBoost(r.created, recencyBoost);
- // Manual entries (no auto-capture tag) get a boost over auto-captured ones.
+ // Human-created entries (webui/cli tags) get a boost over auto-captured and agent entries.
// This ensures intentionally stored knowledge ranks higher than conversation noise.
- const isAutoCapture = r.tags?.includes("auto-capture") ?? false;
- const sourceBoost = isAutoCapture ? 1.0 : manualEntryBoost;
+ const tags = r.tags ?? [];
+ const isHumanCreated = tags.includes("webui") || tags.includes("cli");
+ const sourceBoost = isHumanCreated ? manualEntryBoost : 1.0;
return {
id: r.id,
body: r.content || r.body || "",
diff --git a/palaia/cli.py b/palaia/cli.py
index 2703ec5..9f72518 100644
--- a/palaia/cli.py
+++ b/palaia/cli.py
@@ -212,14 +212,17 @@ def cmd_write(args):
root = get_root()
agent = _resolve_agent(args)
instance = _resolve_instance_for_write(args)
- tags = args.tags.split(",") if args.tags else None
+ tags = args.tags.split(",") if args.tags else []
+ # Tag entries created via CLI so recall can distinguish source
+ if "cli" not in tags:
+ tags.append("cli")
result = write_entry(
root,
body=args.text,
scope=args.scope,
agent=agent,
- tags=tags,
+ tags=tags or None,
title=args.title,
project=getattr(args, "project", None),
entry_type=getattr(args, "type", None),
diff --git a/palaia/web/routes/entries.py b/palaia/web/routes/entries.py
index e32ebdc..fcee801 100644
--- a/palaia/web/routes/entries.py
+++ b/palaia/web/routes/entries.py
@@ -1,9 +1,10 @@
"""Entry CRUD routes.
-v2.6 semantics:
+v2.6+ semantics:
- Tasks are post-its: setting status=done/wontfix on a task deletes it.
-- Auto-capture entries carry the 'auto-capture' tag; manual entries do not.
- Manual entries rank 30% higher in recall (visible via is_manual flag).
+- Source tags: 'webui' (created in browser), 'cli' (palaia add/write),
+ 'auto-capture' (passive capture). No source tag = agent (MCP/tool).
+ Human-created entries (webui, cli) rank 30% higher in recall.
"""
from __future__ import annotations
@@ -57,10 +58,21 @@ def _validate_enum(value: str | None, valid: set[str], field: str) -> str | None
return value
+def _detect_source(tags: list[str]) -> str:
+ """Detect entry source from tags: webui, cli, auto-capture, or agent."""
+ if "webui" in tags:
+ return "webui"
+ if "cli" in tags:
+ return "cli"
+ if "auto-capture" in tags:
+ return "auto"
+ return "agent"
+
+
def _entry_to_dict(meta: dict, body: str, tier: str, *, preview: bool = True) -> dict:
- """Convert store entry to JSON-serializable dict with v2.6 flags."""
+ """Convert store entry to JSON-serializable dict with source flags."""
tags = meta.get("tags", []) or []
- is_auto = "auto-capture" in tags
+ source = _detect_source(tags)
return {
"id": meta.get("id", ""),
"title": meta.get("title", ""),
@@ -78,8 +90,9 @@ def _entry_to_dict(meta: dict, body: str, tier: str, *, preview: bool = True) ->
"accessed": meta.get("accessed", ""),
"access_count": meta.get("access_count", 0),
"decay_score": meta.get("decay_score", 0),
- "is_auto_capture": is_auto,
- "is_manual": not is_auto,
+ "source": source,
+ "is_auto_capture": source == "auto",
+ "is_manual": source in ("webui", "cli"),
"body_preview": (body[:200] + "…") if preview and len(body) > 200 else body,
}
@@ -164,11 +177,13 @@ def get_entry(request: Request, entry_id: str) -> dict:
if "error" in result:
return JSONResponse(status_code=404, content=result)
- # Augment with v2.6 flags
+ # Augment with source flags
meta = result.get("meta", {}) or {}
tags = meta.get("tags", []) or []
- result["is_auto_capture"] = "auto-capture" in tags
- result["is_manual"] = not result["is_auto_capture"]
+ source = _detect_source(tags)
+ result["source"] = source
+ result["is_auto_capture"] = source == "auto"
+ result["is_manual"] = source in ("webui", "cli")
return result
@@ -196,13 +211,18 @@ def create_entry(request: Request, payload: EntryCreate) -> dict:
store = Store(root)
store.recover()
+ # Tag entries created via WebUI so recall can distinguish source
+ tags = list(payload.tags) if payload.tags else []
+ if "webui" not in tags:
+ tags.append("webui")
+
try:
entry_id = store.write(
body=payload.body,
title=payload.title,
entry_type=payload.type,
scope=payload.scope,
- tags=payload.tags or None,
+ tags=tags or None,
project=payload.project,
status=payload.status,
priority=payload.priority,
diff --git a/palaia/web/routes/search.py b/palaia/web/routes/search.py
index d89184c..b05c7b0 100644
--- a/palaia/web/routes/search.py
+++ b/palaia/web/routes/search.py
@@ -78,11 +78,15 @@ def _run():
logger.error("BM25 fallback failed: %s", exc)
result = {"results": [], "has_embeddings": False, "bm25_only": True}
- # Augment results with manual/auto flags
+ # Augment results with source flags
+ from palaia.web.routes.entries import _detect_source
+
for r in result.get("results", []):
tags = r.get("tags", []) or []
- r["is_auto_capture"] = "auto-capture" in tags
- r["is_manual"] = not r["is_auto_capture"]
+ source = _detect_source(tags)
+ r["source"] = source
+ r["is_auto_capture"] = source == "auto"
+ r["is_manual"] = source in ("webui", "cli")
return {
"query": q,
diff --git a/palaia/web/routes/status.py b/palaia/web/routes/status.py
index fe8abb1..49d99a7 100644
--- a/palaia/web/routes/status.py
+++ b/palaia/web/routes/status.py
@@ -104,7 +104,7 @@ def list_tags(request: Request) -> dict:
for meta, _body, _tier in store.all_entries_unfiltered(include_cold=True):
for tag in meta.get("tags") or []:
tag = tag.strip()
- if tag and tag != "auto-capture":
+ if tag and tag not in ("auto-capture", "webui", "cli"):
tags.add(tag)
return {"tags": sorted(tags)}
diff --git a/palaia/web/static/app.js b/palaia/web/static/app.js
index 694f544..7f7490d 100644
--- a/palaia/web/static/app.js
+++ b/palaia/web/static/app.js
@@ -308,15 +308,17 @@
const meta = el("div", { class: "entry-meta" });
meta.appendChild(el("span", { class: "badge badge-tier badge-" + (e.tier || "hot") }, e.tier || "hot"));
meta.appendChild(el("span", { class: "badge badge-type badge-" + (e.type || "memory") }, e.type || "memory"));
- meta.appendChild(el("span", { class: "badge badge-source " + (e.is_manual ? "badge-manual" : "badge-auto") },
- e.is_manual ? "manual ✦" : "auto"));
+ const sourceLabel = { webui: "webui", cli: "cli", auto: "auto", agent: "agent" }[e.source] || "auto";
+ const sourceBadge = e.is_manual ? "badge-manual" : (e.source === "agent" ? "badge-agent-source" : "badge-auto");
+ meta.appendChild(el("span", { class: "badge badge-source " + sourceBadge }, sourceLabel));
if (e.priority) meta.appendChild(el("span", { class: "badge badge-priority-" + e.priority }, e.priority));
if (e.status) meta.appendChild(el("span", { class: "badge badge-status" }, e.status));
if (e.scope && e.scope !== "team") meta.appendChild(el("span", { class: "badge badge-scope" }, e.scope));
if (e.project) meta.appendChild(el("span", { class: "badge badge-project" }, e.project));
if (e.agent) meta.appendChild(el("span", { class: "badge badge-agent" }, "@" + e.agent));
+ const sourceTags = new Set(["auto-capture", "webui", "cli"]);
for (const tag of (e.tags || []).slice(0, 4)) {
- if (tag === "auto-capture") continue; // already shown via source badge
+ if (sourceTags.has(tag)) continue; // already shown via source badge
meta.appendChild(el("span", { class: "tag" }, tag));
}
card.appendChild(meta);
@@ -360,17 +362,25 @@
function renderDetailPanel(id, d) {
const m = d.meta || {};
- const isManual = d.is_manual ?? !(m.tags || []).includes("auto-capture");
+ const source = d.source || (d.is_manual ? "cli" : "auto");
+ const sourceLabels = {
+ webui: "webui (1.3× boost)",
+ cli: "cli (1.3× boost)",
+ auto: "auto-capture",
+ agent: "agent",
+ };
+ const sourceTags = new Set(["auto-capture", "webui", "cli"]);
+ const displayTags = (m.tags || []).filter(t => !sourceTags.has(t));
const rows = [
["Type", m.type || "memory"],
["Scope", m.scope || "team"],
["Tier", m.tier || "—"],
- ["Source", isManual ? "manual (1.3× boost)" : "auto-capture"],
+ ["Source", sourceLabels[source] || source],
["Created", fmtDate(m.created)],
["Accessed", fmtDate(m.accessed)],
["Decay", Number(m.decay_score || 0).toFixed(4)],
- ["Tags", (m.tags || []).join(", ") || "—"],
+ ["Tags", displayTags.join(", ") || "—"],
["Agent", m.agent || "—"],
];
if (m.priority) rows.push(["Priority", m.priority]);
diff --git a/palaia/web/static/style.css b/palaia/web/static/style.css
index 136fd13..cc21f3a 100644
--- a/palaia/web/static/style.css
+++ b/palaia/web/static/style.css
@@ -328,6 +328,7 @@ body {
.badge-process { background: rgba(255,158,100,0.15); color: var(--orange); }
.badge-manual { background: rgba(240,198,116,0.2); color: var(--gold); }
.badge-auto { background: transparent; color: var(--text-dim); border: 1px solid var(--border); }
+.badge-agent-source { background: rgba(122,162,247,0.12); color: var(--accent); }
.badge-scope { background: var(--bg); color: var(--text-dim); border: 1px solid var(--border); }
.badge-agent { background: rgba(122,162,247,0.08); color: var(--text-dim); border: 1px solid var(--border); }
.badge-project { background: rgba(158,206,106,0.1); color: var(--green); }
From 7112ca289464d1370d41bbb87567e5786739afac Mon Sep 17 00:00:00 2001
From: Christian Wendler
Date: Thu, 9 Apr 2026 10:22:47 +0000
Subject: [PATCH 3/3] =?UTF-8?q?fix:=20codex=20review=20=E2=80=94=20markdow?=
=?UTF-8?q?n=20renderer,=20source=20tag=20leaks,=20search=20field?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
P0: Fix renderMarkdown double-encoding in code blocks (extract before
esc), resolve merge conflict from stash/pop.
P0: Filter webui/cli/auto-capture from edit form tag input.
P1: Add source field to search result JS mapping.
P1: Re-apply cli.py source tag injection (lost during stash/pop).
Co-Authored-By: Claude Opus 4.6
---
palaia/web/static/app.js | 57 ++++++++++++++++++++++++----------------
1 file changed, 34 insertions(+), 23 deletions(-)
diff --git a/palaia/web/static/app.js b/palaia/web/static/app.js
index 7f7490d..6dcd89b 100644
--- a/palaia/web/static/app.js
+++ b/palaia/web/static/app.js
@@ -83,34 +83,45 @@
// ── Simple Markdown → HTML ────────────────────────────────────────────────
function renderMarkdown(text) {
if (!text) return "";
- // Escape HTML first (security)
- let html = esc(text);
- // Code blocks (``` ... ```)
- html = html.replace(/```[\s\S]*?```/g, (m) => {
- const code = m.slice(3, -3).replace(/^\w*\n/, "");
- return "" + code + "
";
+ // Extract code blocks BEFORE escaping so their content stays raw.
+ const codeBlocks = [];
+ let safe = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, _lang, code) => {
+ const idx = codeBlocks.length;
+ codeBlocks.push("" + esc(code) + "
");
+ return "\x00CB" + idx + "\x00";
});
- // Inline code
- html = html.replace(/`([^`]+)`/g, "$1");
+ // Extract inline code before escaping
+ const inlineCode = [];
+ safe = safe.replace(/`([^`]+)`/g, (_m, code) => {
+ const idx = inlineCode.length;
+ inlineCode.push("" + esc(code) + "");
+ return "\x00IC" + idx + "\x00";
+ });
+ // Now escape remaining HTML
+ safe = esc(safe);
// Headings
- html = html.replace(/^### (.+)$/gm, "$1
");
- html = html.replace(/^## (.+)$/gm, "$1
");
- html = html.replace(/^# (.+)$/gm, "$1
");
+ safe = safe.replace(/^### (.+)$/gm, "$1
");
+ safe = safe.replace(/^## (.+)$/gm, "$1
");
+ safe = safe.replace(/^# (.+)$/gm, "$1
");
// Bold + italic
- html = html.replace(/\*\*(.+?)\*\*/g, "$1");
- html = html.replace(/\*(.+?)\*/g, "$1");
+ safe = safe.replace(/\*\*(.+?)\*\*/g, "$1");
+ safe = safe.replace(/\*(.+?)\*/g, "$1");
// Blockquotes
- html = html.replace(/^> (.+)$/gm, "$1
");
+ safe = safe.replace(/^> (.+)$/gm, "$1
");
// Unordered lists
- html = html.replace(/^- (.+)$/gm, "$1");
- html = html.replace(/((?:.*<\/li>\n?)+)/g, "");
+ safe = safe.replace(/^- (.+)$/gm, "$1");
+ safe = safe.replace(/((?:.*<\/li>\n?)+)/g, "");
// Ordered lists
- html = html.replace(/^\d+\. (.+)$/gm, "$1");
+ safe = safe.replace(/^(\d+)\. (.+)$/gm, "$2");
+ safe = safe.replace(/((?:.*<\/li>\n?)+)/g, (m) => m.includes("") ? m : "" + m + "
");
// Paragraphs (double newline)
- html = html.replace(/\n\n/g, "");
- // Single newlines within paragraphs →
- html = html.replace(/\n/g, "
");
- return "
" + html + "
";
+ safe = safe.replace(/\n\n/g, "");
+ // Single newlines →
+ safe = safe.replace(/\n/g, "
");
+ // Restore code blocks and inline code
+ safe = safe.replace(/\x00CB(\d+)\x00/g, (_m, idx) => codeBlocks[idx]);
+ safe = safe.replace(/\x00IC(\d+)\x00/g, (_m, idx) => inlineCode[idx]);
+ return "
" + safe + "
";
}
// ── Date formatting ──────────────────────────────────────────────────────
@@ -243,7 +254,7 @@
id: r.id, title: r.title, type: r.type, scope: r.scope, tier: r.tier,
tags: r.tags || [], project: r.project, status: r.status, priority: r.priority,
decay_score: r.decay_score, body_preview: r.body || r.content,
- score: r.score, is_manual: r.is_manual, is_auto_capture: r.is_auto_capture,
+ score: r.score, source: r.source, is_manual: r.is_manual, is_auto_capture: r.is_auto_capture,
agent: r.agent,
}));
state.entries = entries;
@@ -501,7 +512,7 @@
$("form-type").value = m.type || "memory";
$("form-scope").value = m.scope || "team";
$("form-project").value = m.project || "";
- $("form-tags").value = (m.tags || []).filter(t => t !== "auto-capture").join(", ");
+ $("form-tags").value = (m.tags || []).filter(t => !["auto-capture", "webui", "cli"].includes(t)).join(", ");
$("form-agent").value = m.agent || "";
$("form-priority").value = m.priority || "";
$("form-status").value = m.status || "";