From 37bada37b6911b1a1a65a2053244861df13a3555 Mon Sep 17 00:00:00 2001
From: Youzini-afk <13153778771cx@gmail.com>
Date: Mon, 20 Apr 2026 16:39:02 +0800
Subject: [PATCH] feat: add recall card editing and ENA preview UI
---
index.js | 114 +++++++++++++++++++++
style.css | 212 +++++++++++++++++++++++++++++++++++++++
tests/p0-regressions.mjs | 173 ++++++++++++++++++++++++++++++++
ui/recall-message-ui.js | 201 ++++++++++++++++++++++++++++++++++++-
4 files changed, 697 insertions(+), 3 deletions(-)
diff --git a/index.js b/index.js
index 8c912bd..684b59c 100644
--- a/index.js
+++ b/index.js
@@ -3063,6 +3063,102 @@ function editMessageRecallRecord(messageIndex, nextInjectionText) {
return edited;
}
+function syncEditedUserMessageDom(messageIndex, nextText) {
+ const chatRoot = document?.getElementById?.("chat");
+ if (!chatRoot?.querySelectorAll) return false;
+
+ for (const messageElement of Array.from(chatRoot.querySelectorAll(".mes") || [])) {
+ if (resolveMessageIndexFromElement(messageElement) !== messageIndex) continue;
+ const userTextElement = messageElement.querySelector?.(".mes_text");
+ if (!userTextElement) return false;
+ userTextElement.textContent = String(nextText || "");
+ return true;
+ }
+ return false;
+}
+
+function persistEditedUserMessage(context = getContext()) {
+ const candidates = [
+ ["saveChatConditional", context?.saveChatConditional],
+ ["saveChat", context?.saveChat],
+ ];
+
+ for (const [label, handler] of candidates) {
+ if (typeof handler !== "function") continue;
+ try {
+ const result = handler.call(context);
+ if (result && typeof result.catch === "function") {
+ result.catch((error) => {
+ console.error(`[ST-BME] 保存用户输入编辑失败 (${label}):`, error);
+ });
+ }
+ return label;
+ } catch (error) {
+ console.error(`[ST-BME] 调用 ${label} 保存用户输入编辑失败:`, error);
+ }
+ }
+
+ return triggerChatMetadataSave(context, { immediate: true });
+}
+
+function editMessageUserInputText(messageIndex, nextUserInputText) {
+ const context = getContext();
+ const chat = context?.chat;
+ if (!Array.isArray(chat)) {
+ return { ok: false, error: "missing-chat" };
+ }
+
+ const message = chat[messageIndex];
+ if (!message?.is_user) {
+ return { ok: false, error: "not-user-message" };
+ }
+
+ const normalizedText = normalizeRecallInputText(nextUserInputText);
+ if (!normalizedText) {
+ return { ok: false, error: "empty-user-input" };
+ }
+
+ const previousText = normalizeRecallInputText(message.mes || "");
+ const currentRecord = readPersistedRecallFromUserMessage(chat, messageIndex);
+ const recallBoundText = normalizeRecallInputText(
+ currentRecord?.boundUserFloorText || previousText,
+ );
+ const recallMayBeStale = Boolean(currentRecord) && recallBoundText !== normalizedText;
+
+ message.mes = normalizedText;
+ const swipeIndex = Number.isFinite(Number(message?.swipe_id))
+ ? Math.max(0, Math.floor(Number(message.swipe_id)))
+ : null;
+ if (
+ Array.isArray(message?.swipes) &&
+ swipeIndex !== null &&
+ swipeIndex < message.swipes.length
+ ) {
+ message.swipes[swipeIndex] = normalizedText;
+ }
+
+ if (message.extra && typeof message.extra === "object") {
+ if (typeof message.extra.display_text === "string") {
+ message.extra.display_text = normalizedText;
+ }
+ if (typeof message.extra.current_display_text === "string") {
+ message.extra.current_display_text = normalizedText;
+ }
+ }
+
+ const saveMode = persistEditedUserMessage(context);
+ const domSynced = syncEditedUserMessageDom(messageIndex, normalizedText);
+
+ return {
+ ok: true,
+ nextText: normalizedText,
+ recallMayBeStale,
+ unchanged: previousText === normalizedText,
+ saveMode,
+ domSynced,
+ };
+}
+
function rewriteRecallPayloadWithInjection(
promptData = null,
injectionText = "",
@@ -4056,6 +4152,24 @@ function getRecallCardCallbacks() {
},
});
},
+ onEditUserInput: (messageIndex, nextUserInputText) => {
+ const result = editMessageUserInputText(messageIndex, nextUserInputText);
+ if (!result?.ok) {
+ toastr.warning("编辑失败:内容不能为空或此楼层非用户消息");
+ return result;
+ }
+
+ if (result.unchanged) {
+ toastr.info("用户输入未变化");
+ } else {
+ toastr.success("已更新本轮用户输入");
+ }
+ if (result.recallMayBeStale) {
+ toastr.info("输入已改,当前召回结果可能需要重新召回");
+ }
+ schedulePersistedRecallMessageUiRefresh();
+ return result;
+ },
onDelete: (messageIndex) => {
if (removeMessageRecallRecord(messageIndex)) {
toastr.success("已删除持久召回注入");
diff --git a/style.css b/style.css
index 8ae9e8f..d44c53e 100644
--- a/style.css
+++ b/style.css
@@ -5895,6 +5895,7 @@
.bme-recall-user-label {
display: flex;
align-items: center;
+ justify-content: space-between;
gap: 6px;
padding: 10px 14px 4px;
font-size: 12px;
@@ -5902,6 +5903,48 @@
font-weight: 500;
}
+.bme-recall-user-label-text {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+}
+
+.bme-recall-user-label-actions {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ flex-shrink: 0;
+}
+
+.bme-recall-user-edit-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 8px;
+ border: 1px solid var(--bme-border, rgba(255, 255, 255, 0.08));
+ border-radius: 999px;
+ background: transparent;
+ color: var(--bme-on-surface-dim, rgba(228, 225, 230, 0.6));
+ font-size: 11px;
+ cursor: pointer;
+ transition:
+ background 0.15s,
+ border-color 0.15s,
+ color 0.15s;
+}
+
+.bme-recall-user-edit-btn:hover:not(:disabled) {
+ background: var(--bme-surface-high, #2a2a2d);
+ border-color: var(--bme-border-active, rgba(233, 69, 96, 0.4));
+ color: var(--bme-on-surface, #e4e1e6);
+}
+
+.bme-recall-user-edit-btn:disabled {
+ opacity: 0.5;
+ cursor: default;
+}
+
.bme-recall-user-text {
padding: 4px 14px 10px;
font-size: 14px;
@@ -5912,6 +5955,63 @@
word-break: break-word;
}
+.bme-recall-user-edit-wrap {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 4px 14px 10px;
+}
+
+.bme-recall-user-edit-textarea {
+ width: 100%;
+ min-height: 72px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ border: 1px solid var(--bme-border, rgba(255, 255, 255, 0.08));
+ background: var(--bme-surface-low, #1b1b1e);
+ color: var(--bme-on-surface, #e4e1e6);
+ font-size: 13px;
+ line-height: 1.6;
+ resize: vertical;
+ transition: border-color 0.15s;
+}
+
+.bme-recall-user-edit-textarea:focus {
+ outline: none;
+ border-color: var(--bme-border-active, rgba(233, 69, 96, 0.4));
+}
+
+.bme-recall-user-edit-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+}
+
+.bme-recall-user-edit-action {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 68px;
+ padding: 6px 10px;
+ border-radius: 6px;
+ border: 1px solid var(--bme-border, rgba(255, 255, 255, 0.08));
+ background: transparent;
+ color: var(--bme-on-surface-dim, rgba(228, 225, 230, 0.6));
+ font-size: 11px;
+ cursor: pointer;
+}
+
+.bme-recall-user-edit-action.primary {
+ background: var(--bme-primary-dim, rgba(233, 69, 96, 0.15));
+ border-color: var(--bme-primary, #e94560);
+ color: var(--bme-primary-text, #ffb2b7);
+}
+
+.bme-recall-user-edit-action.secondary:hover,
+.bme-recall-user-edit-action.primary:hover {
+ filter: brightness(1.06);
+}
+
.bme-recall-card.bme-recall-hide-user-input .bme-recall-user-label,
.bme-recall-card.bme-recall-hide-user-input .bme-recall-user-text {
display: none;
@@ -6051,6 +6151,103 @@
color: #000;
}
+.bme-recall-meta-tag.is-ena {
+ background: rgba(111, 194, 255, 0.16);
+ color: #9dd6ff;
+ border: 1px solid rgba(111, 194, 255, 0.28);
+}
+
+.bme-recall-meta-text {
+ min-width: 0;
+}
+
+.bme-recall-injection-preview {
+ margin: 0 14px 8px;
+ border: 1px solid var(--bme-border, rgba(255, 255, 255, 0.08));
+ border-radius: 8px;
+ overflow: hidden;
+ background: var(--bme-surface-low, #1b1b1e);
+}
+
+.bme-recall-injection-preview.is-ena {
+ border-left: 3px solid #6fc2ff;
+}
+
+.bme-recall-injection-toggle {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 8px 10px;
+ border: none;
+ background: transparent;
+ color: var(--bme-on-surface, #e4e1e6);
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.bme-recall-injection-toggle-arrow {
+ color: var(--bme-on-surface-dim, rgba(228, 225, 230, 0.6));
+ transition: transform 0.2s ease;
+}
+
+.bme-recall-injection-preview.expanded .bme-recall-injection-toggle-arrow {
+ transform: rotate(90deg);
+}
+
+.bme-recall-injection-content {
+ display: none;
+ padding: 0 10px 10px;
+ border-top: 1px solid var(--bme-border, rgba(255, 255, 255, 0.08));
+}
+
+.bme-recall-injection-preview.expanded .bme-recall-injection-content {
+ display: block;
+}
+
+.bme-recall-injection-note {
+ padding-top: 10px;
+ font-size: 11px;
+ color: #9dd6ff;
+}
+
+.bme-recall-injection-section-title {
+ margin-top: 10px;
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--bme-primary-text, #ffb2b7);
+}
+
+.bme-recall-injection-subsection {
+ margin-top: 8px;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--bme-on-surface, #e4e1e6);
+}
+
+.bme-recall-injection-line,
+.bme-recall-injection-table {
+ margin-top: 6px;
+ font-size: 12px;
+ line-height: 1.55;
+ color: var(--bme-on-surface-dim, rgba(228, 225, 230, 0.78));
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.bme-recall-injection-table {
+ padding: 8px;
+ border-radius: 6px;
+ background: var(--bme-surface-lowest, #0e0e11);
+ overflow-x: auto;
+}
+
+.bme-recall-injection-spacer {
+ height: 6px;
+}
+
/* --- Recall Actions --- */
.bme-recall-actions {
@@ -6312,6 +6509,21 @@
height: 180px;
}
+ .bme-recall-user-label {
+ flex-wrap: wrap;
+ align-items: flex-start;
+ }
+
+ .bme-recall-user-label-actions {
+ width: 100%;
+ justify-content: flex-end;
+ }
+
+ .bme-recall-injection-preview {
+ margin-left: 10px;
+ margin-right: 10px;
+ }
+
.bme-recall-sidebar {
width: 100vw;
}
diff --git a/tests/p0-regressions.mjs b/tests/p0-regressions.mjs
index a9877ea..b4b804b 100644
--- a/tests/p0-regressions.mjs
+++ b/tests/p0-regressions.mjs
@@ -1867,6 +1867,176 @@ async function testRecallCardDisplayModeToggleRestoresOriginalUserText() {
}
}
+async function testRecallCardSupportsManagedUserInputEditing() {
+ const chat = [
+ {
+ is_user: true,
+ mes: "原始用户输入",
+ extra: {
+ bme_recall: buildPersistedRecallRecord({
+ injectionText: "[Memory - Recalled]\nline-1",
+ selectedNodeIds: ["n1"],
+ boundUserFloorText: "原始用户输入",
+ nowIso: "2026-01-01T00:00:00.000Z",
+ }),
+ },
+ },
+ ];
+ const harness = await createRecallUiHarness({ chat });
+ const messageElement = createMessageElement(harness.document, 0, {
+ stableId: true,
+ withMesBlock: true,
+ isUser: true,
+ });
+ const userTextElement = messageElement.querySelector(".mes_text");
+ userTextElement.textContent = chat[0].mes;
+ harness.chatRoot.appendChild(messageElement);
+
+ try {
+ const saveResults = [];
+ const cardModule = await import("../ui/recall-message-ui.js");
+ const mountedCard = cardModule.createRecallCardElement({
+ messageIndex: 0,
+ record: chat[0].extra.bme_recall,
+ userMessageText: chat[0].mes,
+ callbacks: {
+ onEditUserInput: async (_messageIndex, nextText) => {
+ saveResults.push(nextText);
+ return {
+ ok: true,
+ nextText,
+ recallMayBeStale: true,
+ };
+ },
+ },
+ });
+ messageElement.querySelector(".mes_block")?.appendChild(mountedCard);
+
+ const card = harness.chatRoot.querySelector(".bme-recall-card");
+ const editBtn = card.querySelector(".bme-recall-user-edit-btn");
+ assert.equal(Boolean(editBtn), true);
+
+ editBtn.click();
+ const textarea = card.querySelector(".bme-recall-user-edit-textarea");
+ assert.equal(Boolean(textarea), true);
+ assert.equal(card.classList.contains("bme-recall-user-input-editing"), true);
+
+ textarea.value = "新的用户输入";
+ const saveBtn = card.querySelector(".bme-recall-user-edit-action.primary");
+ await saveBtn.dispatchEvent({ type: "click", stopPropagation() {} });
+
+ assert.deepEqual(saveResults, ["新的用户输入"]);
+ assert.equal(chat[0].mes, "原始用户输入");
+ assert.equal(
+ card.querySelector(".bme-recall-user-text")?.textContent,
+ "新的用户输入",
+ );
+ assert.equal(card.classList.contains("bme-recall-user-input-editing"), false);
+ } finally {
+ harness.restoreGlobals();
+ }
+}
+
+async function testRecallCardShowsEnaSourceChipAndExpandedPreview() {
+ const chat = [
+ {
+ is_user: true,
+ mes: "planner user input",
+ extra: {
+ bme_recall: buildPersistedRecallRecord({
+ injectionText: "[Memory - Recalled]\n### 当前状态记忆\nline-1\n|a|b|",
+ selectedNodeIds: ["n1"],
+ recallSource: "planner-handoff",
+ hookName: "ena-planner",
+ nowIso: "2026-01-01T00:00:00.000Z",
+ }),
+ },
+ },
+ ];
+ const harness = await createRecallUiHarness({ chat });
+ const messageElement = createMessageElement(harness.document, 0, {
+ stableId: true,
+ withMesBlock: true,
+ isUser: true,
+ });
+ harness.chatRoot.appendChild(messageElement);
+
+ try {
+ harness.api.refreshPersistedRecallMessageUi();
+ const card = harness.chatRoot.querySelector(".bme-recall-card");
+ card.querySelector(".bme-recall-bar")?.click();
+
+ const enaTag = card.querySelector(".bme-recall-meta-tag.is-ena");
+ assert.equal(Boolean(enaTag), true);
+ assert.equal(enaTag?.textContent, "🧭 ENA Planner");
+
+ const preview = card.querySelector(".bme-recall-injection-preview.is-ena");
+ assert.equal(Boolean(preview), true);
+ assert.equal(preview.classList.contains("expanded"), true);
+ assert.equal(
+ preview.querySelector(".bme-recall-injection-note")?.textContent,
+ "由 Ena Planner 触发的本轮记忆块",
+ );
+ } finally {
+ harness.restoreGlobals();
+ }
+}
+
+async function testRecallCardBeautifiesInjectionPreviewSections() {
+ const chat = [
+ {
+ is_user: true,
+ mes: "normal user input",
+ extra: {
+ bme_recall: buildPersistedRecallRecord({
+ injectionText: "[Memory - Objective / Global]\n#### 子标题\n普通行\n\n|col1|col2|",
+ selectedNodeIds: ["n1"],
+ recallSource: "history",
+ nowIso: "2026-01-01T00:00:00.000Z",
+ }),
+ },
+ },
+ ];
+ const harness = await createRecallUiHarness({ chat });
+ const messageElement = createMessageElement(harness.document, 0, {
+ stableId: true,
+ withMesBlock: true,
+ isUser: true,
+ });
+ harness.chatRoot.appendChild(messageElement);
+
+ try {
+ harness.api.refreshPersistedRecallMessageUi();
+ const card = harness.chatRoot.querySelector(".bme-recall-card");
+ card.querySelector(".bme-recall-bar")?.click();
+
+ const preview = card.querySelector(".bme-recall-injection-preview");
+ assert.equal(Boolean(preview), true);
+ assert.equal(preview.classList.contains("expanded"), false);
+
+ preview.querySelector(".bme-recall-injection-toggle")?.click();
+ assert.equal(preview.classList.contains("expanded"), true);
+ assert.equal(
+ preview.querySelector(".bme-recall-injection-section-title")?.textContent,
+ "[Memory - Objective / Global]",
+ );
+ assert.equal(
+ preview.querySelector(".bme-recall-injection-subsection")?.textContent,
+ "子标题",
+ );
+ assert.equal(
+ preview.querySelector(".bme-recall-injection-line")?.textContent,
+ "普通行",
+ );
+ assert.equal(
+ preview.querySelector(".bme-recall-injection-table")?.textContent,
+ "|col1|col2|",
+ );
+ } finally {
+ harness.restoreGlobals();
+ }
+}
+
function makeEvent(seq, title) {
return createNode({
type: "event",
@@ -7114,6 +7284,9 @@ await testRecallCardRefreshCleansLegacyBadgeAndAvoidsDuplicates();
await testRecallCardExpandedContentRerendersAfterRecordUpdate();
await testRecallCardUserTextRefreshesWithoutCardRecreate();
await testRecallCardDisplayModeToggleRestoresOriginalUserText();
+await testRecallCardSupportsManagedUserInputEditing();
+await testRecallCardShowsEnaSourceChipAndExpandedPreview();
+await testRecallCardBeautifiesInjectionPreviewSections();
await testRecallSubGraphAndDataLayerEntryPoints();
await testRerollUsesBatchBoundaryRollbackAndPersistsState();
await testNotifyHistoryDirtyUsesStageNoticeWithoutGenericWarningToast();
diff --git a/ui/recall-message-ui.js b/ui/recall-message-ui.js
index 4c101e1..5ecfca7 100644
--- a/ui/recall-message-ui.js
+++ b/ui/recall-message-ui.js
@@ -81,7 +81,9 @@ function formatTokenHint(tokenEstimate) {
function formatMetaLine(record) {
const parts = [];
- if (record.recallSource) parts.push(`来源: ${record.recallSource}`);
+ if (record.recallSource && !buildRecallSourceLabel(record)) {
+ parts.push(`来源: ${record.recallSource}`);
+ }
if (record.authoritativeInputUsed) parts.push("权威输入");
if (record.tokenEstimate > 0) parts.push(`~${record.tokenEstimate} tokens`);
if (Number.isFinite(record.generationCount) && record.generationCount > 0) {
@@ -172,6 +174,109 @@ function summarizeSubGraphForSignature(subGraph) {
return { nodes, edges };
}
+function isPlannerRecallSource(record = {}) {
+ const recallSource = String(record?.recallSource || "").trim().toLowerCase();
+ const hookName = String(record?.hookName || "").trim().toLowerCase();
+ return recallSource.startsWith("planner") || hookName.includes("planner") || hookName.includes("ena");
+}
+
+function buildRecallSourceLabel(record = {}) {
+ if (isPlannerRecallSource(record)) return "ENA Planner";
+ return "";
+}
+
+function classifyInjectionLine(line = "") {
+ const text = String(line || "");
+ const trimmed = text.trim();
+ if (!trimmed) return { kind: "blank", text };
+ if (/^\[[^\]]+\]$/.test(trimmed)) return { kind: "section", text: trimmed };
+ if (/^#{2,6}\s+/.test(trimmed)) {
+ return { kind: "subsection", text: trimmed.replace(/^#{2,6}\s+/, "") };
+ }
+ if (/^\|.*\|$/.test(trimmed)) return { kind: "table", text };
+ return { kind: "line", text };
+}
+
+function appendInjectionPreviewContent(container, injectionText = "") {
+ const lines = String(injectionText || "").replace(/\r\n/g, "\n").split("\n");
+ let tableBuffer = [];
+
+ const flushTable = () => {
+ if (tableBuffer.length === 0) return;
+ const pre = el("pre", "bme-recall-injection-table", tableBuffer.join("\n"));
+ container.appendChild(pre);
+ tableBuffer = [];
+ };
+
+ for (const rawLine of lines) {
+ const classified = classifyInjectionLine(rawLine);
+ if (classified.kind === "table") {
+ tableBuffer.push(classified.text);
+ continue;
+ }
+
+ flushTable();
+
+ if (classified.kind === "blank") {
+ container.appendChild(el("div", "bme-recall-injection-spacer"));
+ continue;
+ }
+ if (classified.kind === "section") {
+ container.appendChild(
+ el("div", "bme-recall-injection-section-title", classified.text),
+ );
+ continue;
+ }
+ if (classified.kind === "subsection") {
+ container.appendChild(
+ el("div", "bme-recall-injection-subsection", classified.text),
+ );
+ continue;
+ }
+ container.appendChild(el("div", "bme-recall-injection-line", classified.text));
+ }
+
+ flushTable();
+}
+
+function buildInjectionPreviewBlock(record = {}) {
+ const injectionText = String(record?.injectionText || "").trim();
+ if (!injectionText) return null;
+
+ const isEna = isPlannerRecallSource(record);
+ const wrap = el(
+ "div",
+ `bme-recall-injection-preview${isEna ? " is-ena" : ""}`,
+ );
+ const header = el("button", "bme-recall-injection-toggle");
+ header.type = "button";
+ const defaultExpanded = isEna;
+ header.setAttribute("aria-expanded", defaultExpanded ? "true" : "false");
+ header.innerHTML = `
+ ${isEna ? "ENA 注入预览" : "注入预览"}
+ ▶
+ `;
+ wrap.appendChild(header);
+
+ const content = el("div", "bme-recall-injection-content");
+ if (isEna) {
+ content.appendChild(
+ el("div", "bme-recall-injection-note", "由 Ena Planner 触发的本轮记忆块"),
+ );
+ }
+ appendInjectionPreviewContent(content, injectionText);
+ wrap.appendChild(content);
+ wrap.classList.toggle("expanded", defaultExpanded);
+
+ header.addEventListener("click", (event) => {
+ event.stopPropagation();
+ const expanded = wrap.classList.toggle("expanded");
+ header.setAttribute("aria-expanded", expanded ? "true" : "false");
+ });
+
+ return wrap;
+}
+
function buildExpandedRenderSignature({
record,
userMessageText,
@@ -231,15 +336,39 @@ export function createRecallCardElement({
userInputDisplayMode,
);
let expandedRenderSignature = "";
+ let isEditingUserInput = false;
// -- 用户消息区 --
const userLabel = el("div", "bme-recall-user-label");
- userLabel.innerHTML = "💬 本轮用户输入";
+ const userLabelText = el("div", "bme-recall-user-label-text");
+ userLabelText.innerHTML = "💬 本轮用户输入";
+ userLabel.appendChild(userLabelText);
+
+ const userLabelActions = el("div", "bme-recall-user-label-actions");
+ const editUserInputBtn = el("button", "bme-recall-user-edit-btn");
+ editUserInputBtn.type = "button";
+ editUserInputBtn.innerHTML = '✏️编辑';
+ userLabelActions.appendChild(editUserInputBtn);
+ userLabel.appendChild(userLabelActions);
card.appendChild(userLabel);
const userText = el("div", "bme-recall-user-text", activeUserMessageText || "(empty)");
card.appendChild(userText);
+ const userEditWrap = el("div", "bme-recall-user-edit-wrap");
+ const userEditTextarea = document.createElement("textarea");
+ userEditTextarea.className = "bme-recall-user-edit-textarea";
+ userEditWrap.appendChild(userEditTextarea);
+ const userEditActions = el("div", "bme-recall-user-edit-actions");
+ const userEditSaveBtn = el("button", "bme-recall-user-edit-action primary", "保存");
+ userEditSaveBtn.type = "button";
+ const userEditCancelBtn = el("button", "bme-recall-user-edit-action secondary", "取消");
+ userEditCancelBtn.type = "button";
+ userEditActions.appendChild(userEditSaveBtn);
+ userEditActions.appendChild(userEditCancelBtn);
+ userEditWrap.appendChild(userEditActions);
+ card.appendChild(userEditWrap);
+
// -- 召回条 --
const initialNodeCount = Array.isArray(activeRecord?.selectedNodeIds)
? activeRecord.selectedNodeIds.length
@@ -279,6 +408,22 @@ export function createRecallCardElement({
// renderer 实例管理
let renderer = null;
+ function setUserInputEditMode(editing = false) {
+ isEditingUserInput = Boolean(editing);
+ card.classList.toggle("bme-recall-user-input-editing", isEditingUserInput);
+ userText.hidden = isEditingUserInput;
+ userEditWrap.hidden = !isEditingUserInput;
+ editUserInputBtn.disabled = isEditingUserInput;
+ if (!isEditingUserInput) return;
+
+ userEditTextarea.value = activeUserMessageText || "";
+ const lineCount = Math.max(3, String(activeUserMessageText || "").split(/\n/).length);
+ if (userEditTextarea.style && typeof userEditTextarea.style === "object") {
+ userEditTextarea.style.minHeight = `${Math.min(12, lineCount) * 22}px`;
+ }
+ userEditTextarea.focus?.();
+ }
+
function destroyRenderer() {
if (renderer) {
renderer.stopAnimation();
@@ -332,13 +477,34 @@ export function createRecallCardElement({
}
// 元信息行
- const meta = el("div", "bme-recall-meta", formatMetaLine(activeRecord || {}));
+ const meta = el("div", "bme-recall-meta");
+ const sourceLabel = buildRecallSourceLabel(activeRecord || {});
+ const metaText = formatMetaLine(activeRecord || {});
+ if (typeof HTMLElement === "undefined" || !(meta instanceof HTMLElement)) {
+ meta.textContent = metaText;
+ }
+ if (sourceLabel) {
+ const sourceTag = el(
+ "span",
+ `bme-recall-meta-tag${isPlannerRecallSource(activeRecord) ? " is-ena" : ""}`,
+ isPlannerRecallSource(activeRecord) ? `🧭 ${sourceLabel}` : sourceLabel,
+ );
+ meta.appendChild(sourceTag);
+ }
+ if (metaText) {
+ meta.appendChild(el("span", "bme-recall-meta-text", metaText));
+ }
if (activeRecord?.manuallyEdited) {
const tag = el("span", "bme-recall-meta-tag", "✍ 手动编辑");
meta.appendChild(tag);
}
body.appendChild(meta);
+ const injectionPreviewBlock = buildInjectionPreviewBlock(activeRecord || {});
+ if (injectionPreviewBlock) {
+ body.appendChild(injectionPreviewBlock);
+ }
+
// 操作按钮行
const actions = el("div", "bme-recall-actions");
@@ -413,6 +579,9 @@ export function createRecallCardElement({
activeUserInputDisplayMode === "off",
);
userText.textContent = activeUserMessageText || "(empty)";
+ if (isEditingUserInput) {
+ userEditTextarea.value = activeUserMessageText || "";
+ }
const nodeCount = Array.isArray(activeRecord?.selectedNodeIds)
? activeRecord.selectedNodeIds.length
@@ -439,6 +608,31 @@ export function createRecallCardElement({
card._bmeUpdateRecallCard = applyCardRuntimeData;
+ editUserInputBtn.addEventListener("click", (event) => {
+ event.stopPropagation();
+ setUserInputEditMode(true);
+ });
+
+ userEditCancelBtn.addEventListener("click", (event) => {
+ event.stopPropagation();
+ setUserInputEditMode(false);
+ });
+
+ userEditSaveBtn.addEventListener("click", async (event) => {
+ event.stopPropagation();
+ const result = await activeCallbacks.onEditUserInput?.(
+ messageIndex,
+ userEditTextarea.value,
+ );
+ if (result?.ok) {
+ if (Object.prototype.hasOwnProperty.call(result, "nextText")) {
+ activeUserMessageText = String(result.nextText || "");
+ userText.textContent = activeUserMessageText || "(empty)";
+ }
+ setUserInputEditMode(false);
+ }
+ });
+
// 点击召回条 toggle 展开/折叠
bar.addEventListener("click", (e) => {
e.stopPropagation();
@@ -455,6 +649,7 @@ export function createRecallCardElement({
});
applyCardRuntimeData({}, { skipExpandedRerender: true });
+ setUserInputEditMode(false);
// 暴露清理方法
card._bmeDestroyRenderer = () => {