feat: advance local-first conflict and search indexing
- 补齐本地 markdown 冲突处理与合并写回路径\n- 增加本地搜索索引路由、刷新与 browser smoke\n- 同步更新 current-priority checklist 的阶段进度
This commit is contained in:
@@ -1571,6 +1571,27 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
return flattenText(toTiptapDocument(body.content)).replace(/\n{3,}/g, '\n\n').trim();
|
||||
};
|
||||
|
||||
const conflictEnvelopeFromResponse = (payload) => (
|
||||
payload?.error?.details?.conflict
|
||||
|| payload?.details?.conflict
|
||||
|| null
|
||||
);
|
||||
|
||||
const conflictSourceLabel = (session) => {
|
||||
if (!session) return '';
|
||||
if (session.lastExternalWriteSource === 'mnote-hermes-tool') {
|
||||
const runId = String(session.lastExternalWriteRunId || '').trim();
|
||||
return runId ? `agent run ${runId}` : 'agent run';
|
||||
}
|
||||
return '本地文件变更';
|
||||
};
|
||||
|
||||
const conflictMessageFromEnvelope = (session, envelope) => {
|
||||
const baseMessage = envelope?.message || externalConflictMessage;
|
||||
const sourceLabel = conflictSourceLabel(session);
|
||||
return sourceLabel ? `${baseMessage}(来源:${sourceLabel})` : baseMessage;
|
||||
};
|
||||
|
||||
const clearSessionConflictSurface = (session) => {
|
||||
sessionViews(session).forEach((view) => {
|
||||
const host = view.runtimeDescriptor.root.closest('.document-pane') || view.runtimeDescriptor.root;
|
||||
@@ -1633,7 +1654,42 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
currentBox.append(currentTitle, current);
|
||||
const diskBox = document.createElement('section');
|
||||
diskBox.append(diskTitle, disk);
|
||||
diffPanel.append(currentBox, diskBox);
|
||||
const mergeTitle = document.createElement('h3');
|
||||
mergeTitle.textContent = '合并结果';
|
||||
const mergeText = document.createElement('textarea');
|
||||
mergeText.setAttribute('data-testid', 'mnote-conflict-merge-text');
|
||||
mergeText.value = sessionPlainText(session) || aggregatePlainText(latest) || '';
|
||||
const mergeActions = document.createElement('div');
|
||||
mergeActions.className = 'mnote-conflict-actions';
|
||||
const useCurrent = document.createElement('button');
|
||||
useCurrent.type = 'button';
|
||||
useCurrent.textContent = '使用当前版本';
|
||||
useCurrent.setAttribute('data-testid', 'mnote-conflict-merge-use-current');
|
||||
const useDisk = document.createElement('button');
|
||||
useDisk.type = 'button';
|
||||
useDisk.textContent = '使用磁盘版本';
|
||||
useDisk.setAttribute('data-testid', 'mnote-conflict-merge-use-disk');
|
||||
const saveMerge = document.createElement('button');
|
||||
saveMerge.type = 'button';
|
||||
saveMerge.textContent = '写回合并结果';
|
||||
saveMerge.setAttribute('data-testid', 'mnote-conflict-merge-save');
|
||||
mergeActions.append(useCurrent, useDisk, saveMerge);
|
||||
const mergeBox = document.createElement('section');
|
||||
mergeBox.className = 'mnote-conflict-merge-box';
|
||||
mergeBox.append(mergeTitle, mergeText, mergeActions);
|
||||
diffPanel.append(currentBox, diskBox, mergeBox);
|
||||
useCurrent.addEventListener('click', () => {
|
||||
mergeText.value = sessionPlainText(session) || '';
|
||||
});
|
||||
useDisk.addEventListener('click', () => {
|
||||
mergeText.value = aggregatePlainText(latest) || '';
|
||||
});
|
||||
saveMerge.addEventListener('click', () => {
|
||||
writeMergedConflictResult(session, panel, mergeText.value).catch((error) => {
|
||||
setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error));
|
||||
renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
loading.textContent = error instanceof Error ? error.message : String(error);
|
||||
diffPanel.replaceChildren(loading);
|
||||
@@ -1673,6 +1729,24 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
await persistSession(session);
|
||||
};
|
||||
|
||||
const writeMergedConflictResult = async (session, panel, mergedText) => {
|
||||
setSessionStatus(session, 'conflict-resolving', '正在写回合并结果...');
|
||||
const latest = await fetchLatestSessionAggregate(session);
|
||||
const nextKey = conflictDetectionKeyFromBody(latest.body || {});
|
||||
if (nextKey) {
|
||||
session.conflictDetectionKey = nextKey;
|
||||
session.lastExternalConflictDetectionKey = nextKey;
|
||||
}
|
||||
session.currentTiptapDocument = textToTiptapDocument(String(mergedText || ''));
|
||||
session.currentSerialized = JSON.stringify(session.currentTiptapDocument);
|
||||
session.hasExternalConflict = false;
|
||||
session.externalChangePending = false;
|
||||
session.saving = false;
|
||||
session.dirty = true;
|
||||
if (panel && typeof panel.remove === 'function') panel.remove();
|
||||
await persistSession(session);
|
||||
};
|
||||
|
||||
const renderSessionConflictSurface = (session, message) => {
|
||||
clearSessionConflictSurface(session);
|
||||
sessionViews(session).forEach((view) => {
|
||||
@@ -1690,7 +1764,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
text.textContent = message || externalConflictMessage;
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'mnote-conflict-meta';
|
||||
meta.textContent = `文件:${session.rootUri || session.documentId} · 来源:本地文件变更`;
|
||||
meta.textContent = `文件:${session.rootUri || session.documentId} · 来源:${conflictSourceLabel(session) || '本地文件变更'}`;
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'mnote-conflict-actions';
|
||||
const acceptDisk = document.createElement('button');
|
||||
@@ -1744,8 +1818,9 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
window.clearTimeout(session.saveTimer);
|
||||
session.saveTimer = 0;
|
||||
}
|
||||
setSessionStatus(session, 'external-change-conflict', message || externalConflictMessage);
|
||||
renderSessionConflictSurface(session, message || externalConflictMessage);
|
||||
const nextMessage = message || externalConflictMessage;
|
||||
setSessionStatus(session, 'external-change-conflict', nextMessage);
|
||||
renderSessionConflictSurface(session, nextMessage);
|
||||
};
|
||||
|
||||
const queueSessionSave = (session) => {
|
||||
@@ -1801,6 +1876,13 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
const result = await response.json().catch(() => null);
|
||||
if (!response.ok || !result || result.ok !== true) {
|
||||
const message = result?.error?.message || result?.message || `save_failed_${response.status}`;
|
||||
const conflictEnvelope = conflictEnvelopeFromResponse(result);
|
||||
if (response.status === 409 && conflictEnvelope && session.sourceKind === 'local_folder') {
|
||||
session.saving = false;
|
||||
session.lastExternalConflictEnvelope = conflictEnvelope;
|
||||
markSessionExternalConflict(session, conflictMessageFromEnvelope(session, conflictEnvelope));
|
||||
return;
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
const saved = result.result || {};
|
||||
@@ -1879,6 +1961,12 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
return;
|
||||
}
|
||||
const payload = await response.json();
|
||||
const conflictEnvelope = conflictEnvelopeFromResponse(payload);
|
||||
if (response.status === 409 && conflictEnvelope) {
|
||||
session.lastExternalConflictEnvelope = conflictEnvelope;
|
||||
markSessionExternalConflict(session, conflictMessageFromEnvelope(session, conflictEnvelope));
|
||||
return;
|
||||
}
|
||||
const nextAggregate = payload?.result;
|
||||
const nextBody = nextAggregate?.body || {};
|
||||
const nextPermissions = nextAggregate?.head?.permissions || {};
|
||||
@@ -2137,8 +2225,11 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
if (!sessionMatchesDocumentWorkspace(session, documentId, workspaceId)) return;
|
||||
session.lastExternalChangeSignalAt = Date.now();
|
||||
session.externalChangePending = true;
|
||||
session.lastExternalWriteSource = source || session.lastExternalWriteSource || '';
|
||||
session.lastExternalWriteRunId = String(detail?.runId || detail?.traceId || detail?.toolCallId || '').trim();
|
||||
session.lastExternalWriteDocumentId = documentId;
|
||||
if (session.hasExternalConflict || sessionHasRecentLocalInput(session) || session.dirty || session.saveTimer || session.saving) {
|
||||
markSessionExternalConflict(session, treeExternalConflictMessage);
|
||||
markSessionExternalConflict(session, conflictMessageFromEnvelope(session, session.lastExternalConflictEnvelope) || treeExternalConflictMessage);
|
||||
return;
|
||||
}
|
||||
scheduled += 1;
|
||||
@@ -3676,6 +3767,9 @@ mod tests {
|
||||
assert!(html.contains("mnote-conflict-accept-disk"));
|
||||
assert!(html.contains("mnote-conflict-keep-current"));
|
||||
assert!(html.contains("mnote-conflict-open-diff"));
|
||||
assert!(html.contains("mnote-conflict-merge-text"));
|
||||
assert!(html.contains("mnote-conflict-merge-save"));
|
||||
assert!(html.contains("agent run"));
|
||||
assert!(!html.contains(
|
||||
"setInterval(() => {\n void pollLocalMarkdownExternalChange();\n }, 1200);"
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user