refactor: extract conflict diff panel runtime

This commit is contained in:
lix-2026
2026-05-25 02:44:19 +08:00
parent 9d1ec75c6f
commit 61727e17b1
3 changed files with 187 additions and 53 deletions
@@ -6,7 +6,7 @@
## 1. 目标
`3-19` 已归档的基础上,继续把仍留在 Rust inline / 主壳大 runtime 中的浏览器行为切成可审计模块。Codex 维护本文件作为事实源,Reasonix 只承接短批次审计、窄范围实现或浏览器 smoke;每批最多 2 个 worker。
`3-19` 已归档的基础上,继续把仍留在 Rust inline / 主壳大 runtime 中的浏览器行为切成可审计模块。Codex 维护本文件作为事实源,Reasonix 只承接短批次审计、窄范围实现或浏览器 smoke;每批最多 4 个 worker。
## 2. 本轮边界
@@ -92,7 +92,7 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
- 修改:`rust/crates/mnote-web/browser/document-conflict-panel-runtime.js``rust/crates/mnote-web/src/routes/web_shell.rs`
- 已验证:`node --check rust/crates/mnote-web/browser/document-conflict-panel-runtime.js``cargo fmt --manifest-path rust/Cargo.toml --all --check``cargo check --manifest-path rust/Cargo.toml -p mnote-web``cargo test --manifest-path rust/Cargo.toml -p mnote-web document_conflict_panel_runtime_contains_dom_helpers -- --test-threads=1``cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1`
- 未完成:conflict merge / accept / keep 业务逻辑、document session lifecycle、secondary pane conflict smoke 仍未迁出;本轮未声称真实冲突 UI 全链路完成。
- 2026-05-25:根据用户纠正,后续实现 worker 不能在主工作区直接修改;每批最多 2 个 worker,且必须采用独立 worktree 候选 patch 或明确只读任务。Codex 读取 Hindsight recall、`process-handoff.md/json``result.json`、diff 与验证证据后再选择性合入。
- 2026-05-25:根据用户纠正,后续实现 worker 不能在主工作区直接修改;每批最多 4 个 worker,且必须采用独立 worktree 候选 patch 或明确只读任务。Codex 读取 Hindsight recall、`process-handoff.md/json``result.json`、diff 与验证证据后再选择性合入。
- 2026-05-25Codex 主控完成非本地 media upload API 小切片:`local-upload-runtime.js` 新增并导出 `uploadMediaAsset(file, plan, context)`,承接 `/api/media/upload` 的 FormData 构造、fetch 和响应解析;`layout.rs``uploadFileToMediaAsset` 仍负责 appendUploadedAssetRow、editor 插入和 `wolai:assets-changed` 事件分发,并保留完整 inline fallback。
- 修改:`rust/crates/mnote-web/browser/local-upload-runtime.js``rust/crates/mnote-web/src/ssr/pages/layout.rs`
- 已验证:`node --check rust/crates/mnote-web/browser/local-upload-runtime.js``cargo fmt --manifest-path rust/Cargo.toml --all --check``cargo check --manifest-path rust/Cargo.toml -p mnote-web``cargo test --manifest-path rust/Cargo.toml -p mnote-web local_upload_runtime_contains_editor_upload_context_helpers -- --test-threads=1``cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder -- --test-threads=1``node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
@@ -114,4 +114,12 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
- 过程说明:Worker B 在独立 worktree 中复跑 `task479` 时曾出现 `broken-link-after-real-file-delete` 等待超时且 JSON `ok:false`;Codex 在主工作区移植后复跑同一 smoke,结果 `ok:true`,截图证据位于该 smoke 输出的临时目录。
- 未完成:`resolveFileTreeUploadTarget`、filetree DOM runtime 的 `refreshLocalFolderSidebarSnapshot` / `appendUploadedAssetRow` 更细拆分、conflict merge / accept / keep 业务逻辑、secondary pane conflict smoke 仍未迁出。
- 2026-05-25Batch E 计划继续采用独立 worktree 候选 patchWorker B 负责把 document conflict diff / merge 区域的纯 DOM 构建和按钮 wiring 外置到 `document-conflict-panel-runtime.js``web_shell.rs` 仍持有 fetch、plain text 计算、status 和 persist 业务逻辑,避免和当前 local upload 写集冲突。
- 2026-05-25Batch E 派发异常并终止:Worker B 任务被重复启动为两个 runner / ACP 进程(`reasonix-2026-05-24T18-25-26-844Z-33ea4d58``reasonix-2026-05-24T18-26-04-453Z-5ff62e40`),同一 worktree `/mnt/Data1T/mnote-worktrees/0525-e-worker-b-3-20-conflict-diff-dom` 出现并发写入风险;Codex 已终止相关 Reasonix 进程,未读取到 `final.md``result.json``process-handoff.md/json`,该 diff 只保留为后续人工审计素材,不采纳
- 2026-05-25Codex 独立复核、修正并移植 Batch E Worker B 候选 diff`document-conflict-panel-runtime.js` 新增并导出 `populateSessionConflictDiffPanel(panel, context, deps)`,只负责 current/disk/merge DOM 构建和按钮 wiring`web_shell.rs``openConflictDiffPanel` 仍负责 fetch 最新快照、plain text 计算、错误显示和 `writeMergedConflictResult` 持久化回调,inline fallback 保持完整。Codex 修正了候选 diff 中“展示占位文案可能被 Use Current / Use Disk 写回 merge textarea”的语义偏差,新增 `currentMergeText` / `diskMergeText` 和可选 `onUseCurrent` / `onUseDisk`
- Reasonix 协作证据:runner `reasonix-2026-05-24T18-25-26-844Z-33ea4d58` / `reasonix-2026-05-24T18-26-04-453Z-5ff62e40` 均仅产出 `prompt.md` / `memory-recall.json` / `reasonix-transcript.jsonl`,无 `final.md``result.json``process-handoff.md/json`;因此不作为完整 handoff 采纳。Codex 已查 Hindsight recall、核对 worktree diff 与修改范围后移植。
- 候选 worktree`/mnt/Data1T/mnote-worktrees/0525-e-worker-b-3-20-conflict-diff-dom`
- 修改:`rust/crates/mnote-web/browser/document-conflict-panel-runtime.js``rust/crates/mnote-web/src/routes/web_shell.rs`
- 已验证:`node --check rust/crates/mnote-web/browser/document-conflict-panel-runtime.js``cargo fmt --manifest-path rust/Cargo.toml --all --check``cargo check --manifest-path rust/Cargo.toml -p mnote-web``cargo test --manifest-path rust/Cargo.toml -p mnote-web document_conflict_panel_runtime_contains_dom_helpers -- --test-threads=1``cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1``NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task451-local-markdown-conflict-resolution-ui-smoke.js`
- 同轮回归:`local_upload_runtime_contains_editor_upload_context_helpers``sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder``task479-local-folder-markdown-resource-lifecycle-smoke` 仍通过。
- 未完成:`acceptDiskVersion``keepCurrentEditorVersion``writeMergedConflictResult`、document session lifecycle、secondary pane conflict smoke 仍未迁出或覆盖。
- 2026-05-25Batch E 派发异常并终止:Worker B 任务被重复启动为两个 runner / ACP 进程(`reasonix-2026-05-24T18-25-26-844Z-33ea4d58``reasonix-2026-05-24T18-26-04-453Z-5ff62e40`),同一 worktree `/mnt/Data1T/mnote-worktrees/0525-e-worker-b-3-20-conflict-diff-dom` 出现并发写入风险;Codex 已终止相关 Reasonix 进程,未读取到 `final.md``result.json``process-handoff.md/json`。原始未验证 diff 不直接采纳;后续采纳必须经过 Codex 独立复核、修正和验证。
- 2026-05-25Batch F 计划继续采用独立 worktree 候选 patchWorker B 负责把 file tree 上传目标解析的纯 fallback / plan 归一化 helper 外置到 `filetree-runtime.js``layout.rs` 仍保留 async preflight 和完整 inline fallback,避免和当前 conflict panel 写集冲突。
@@ -81,7 +81,94 @@ function createSessionConflictPanel(session, message, deps) {
return panel;
}
/**
* 在已有 diffPanel 中填充 current/disk/merge DOM 并绑定回调。
* 不处理 fetch、persist、status 等业务逻辑。
* @param {HTMLElement} panel - 已清空的 diffPanel 元素
* @param {Object} context
* @param {string} context.currentText - 当前编辑器的文本(含占位文案)
* @param {string} context.diskText - 磁盘版本的文本(含占位文案)
* @param {string} [context.currentMergeText] - 写入合并区域的当前编辑器原始文本
* @param {string} [context.diskMergeText] - 写入合并区域的磁盘版本原始文本
* @param {string} [context.mergeDefaultText] - 合并区域默认文本
* @param {Object} deps
* @param {Function} [deps.onUseCurrent] - 可选:返回当前版本写入合并区域的文本
* @param {Function} [deps.onUseDisk] - 可选:返回磁盘版本写入合并区域的文本
* @param {Function} deps.onSaveMerge - 保存合并结果回调,接收合并文本字符串
*/
function populateSessionConflictDiffPanel(panel, context, deps) {
context = context || {};
deps = deps || {};
var currentText = context.currentText || '';
var diskText = context.diskText || '';
var currentMergeText = Object.prototype.hasOwnProperty.call(context, 'currentMergeText')
? String(context.currentMergeText || '')
: currentText;
var diskMergeText = Object.prototype.hasOwnProperty.call(context, 'diskMergeText')
? String(context.diskMergeText || '')
: diskText;
var mergeDefaultText = context.mergeDefaultText || currentText || diskText || '';
var current = document.createElement('pre');
current.setAttribute('data-testid', 'mnote-conflict-current-text');
current.textContent = currentText;
var disk = document.createElement('pre');
disk.setAttribute('data-testid', 'mnote-conflict-disk-text');
disk.textContent = diskText;
var currentTitle = document.createElement('h3');
currentTitle.textContent = '当前编辑器版本';
var diskTitle = document.createElement('h3');
diskTitle.textContent = '磁盘版本';
var currentBox = document.createElement('section');
currentBox.append(currentTitle, current);
var diskBox = document.createElement('section');
diskBox.append(diskTitle, disk);
var mergeTitle = document.createElement('h3');
mergeTitle.textContent = '合并结果';
var mergeText = document.createElement('textarea');
mergeText.setAttribute('data-testid', 'mnote-conflict-merge-text');
mergeText.value = mergeDefaultText;
var mergeActions = document.createElement('div');
mergeActions.className = 'mnote-conflict-actions';
var useCurrent = document.createElement('button');
useCurrent.type = 'button';
useCurrent.textContent = '使用当前版本';
useCurrent.setAttribute('data-testid', 'mnote-conflict-merge-use-current');
var useDisk = document.createElement('button');
useDisk.type = 'button';
useDisk.textContent = '使用磁盘版本';
useDisk.setAttribute('data-testid', 'mnote-conflict-merge-use-disk');
var saveMerge = document.createElement('button');
saveMerge.type = 'button';
saveMerge.textContent = '写回合并结果';
saveMerge.setAttribute('data-testid', 'mnote-conflict-merge-save');
mergeActions.append(useCurrent, useDisk, saveMerge);
var mergeBox = document.createElement('section');
mergeBox.className = 'mnote-conflict-merge-box';
mergeBox.append(mergeTitle, mergeText, mergeActions);
panel.append(currentBox, diskBox, mergeBox);
useCurrent.addEventListener('click', function() {
var nextText = typeof deps.onUseCurrent === 'function'
? deps.onUseCurrent(context)
: currentMergeText;
mergeText.value = typeof nextText === 'string' ? nextText : currentMergeText;
});
useDisk.addEventListener('click', function() {
var nextText = typeof deps.onUseDisk === 'function'
? deps.onUseDisk(context)
: diskMergeText;
mergeText.value = typeof nextText === 'string' ? nextText : diskMergeText;
});
saveMerge.addEventListener('click', function() {
if (typeof deps.onSaveMerge === 'function') {
deps.onSaveMerge(mergeText.value);
}
});
}
window.__mnoteDocumentConflictPanelRuntime = {
clearSessionConflictSurface: clearSessionConflictSurface,
createSessionConflictPanel: createSessionConflictPanel
createSessionConflictPanel: createSessionConflictPanel,
populateSessionConflictDiffPanel: populateSessionConflictDiffPanel
};
+88 -49
View File
@@ -1978,56 +1978,80 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
? resourceSnapshotPlainText(latest)
: aggregatePlainText(latest);
diffPanel.replaceChildren();
const current = document.createElement('pre');
current.setAttribute('data-testid', 'mnote-conflict-current-text');
current.textContent = sessionPlainText(session) || '(当前编辑器为空)';
const disk = document.createElement('pre');
disk.setAttribute('data-testid', 'mnote-conflict-disk-text');
disk.textContent = latestText || '(磁盘版本为空)';
const currentTitle = document.createElement('h3');
currentTitle.textContent = '当前编辑器版本';
const diskTitle = document.createElement('h3');
diskTitle.textContent = '磁盘版本';
const currentBox = document.createElement('section');
currentBox.append(currentTitle, current);
const diskBox = document.createElement('section');
diskBox.append(diskTitle, disk);
const mergeTitle = document.createElement('h3');
mergeTitle.textContent = '合并结果';
const mergeText = document.createElement('textarea');
mergeText.setAttribute('data-testid', 'mnote-conflict-merge-text');
mergeText.value = sessionPlainText(session) || latestText || '';
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 = latestText || '';
});
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));
var _dpRT_ = window.__mnoteDocumentConflictPanelRuntime;
if (_dpRT_ && typeof _dpRT_.populateSessionConflictDiffPanel === 'function') {
_dpRT_.populateSessionConflictDiffPanel(diffPanel, {
currentText: sessionPlainText(session) || '(当前编辑器为空)',
diskText: latestText || '(磁盘版本为空)',
currentMergeText: sessionPlainText(session) || '',
diskMergeText: latestText || '',
mergeDefaultText: sessionPlainText(session) || latestText || '',
}, {
onUseCurrent: function() {
return sessionPlainText(session) || '';
},
onUseDisk: function() {
return latestText || '';
},
onSaveMerge: function(mergeText) {
writeMergedConflictResult(session, panel, mergeText).catch((error) => {
setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error));
renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error));
});
},
});
});
} else {
const current = document.createElement('pre');
current.setAttribute('data-testid', 'mnote-conflict-current-text');
current.textContent = sessionPlainText(session) || '(当前编辑器为空)';
const disk = document.createElement('pre');
disk.setAttribute('data-testid', 'mnote-conflict-disk-text');
disk.textContent = latestText || '(磁盘版本为空)';
const currentTitle = document.createElement('h3');
currentTitle.textContent = '当前编辑器版本';
const diskTitle = document.createElement('h3');
diskTitle.textContent = '磁盘版本';
const currentBox = document.createElement('section');
currentBox.append(currentTitle, current);
const diskBox = document.createElement('section');
diskBox.append(diskTitle, disk);
const mergeTitle = document.createElement('h3');
mergeTitle.textContent = '合并结果';
const mergeText = document.createElement('textarea');
mergeText.setAttribute('data-testid', 'mnote-conflict-merge-text');
mergeText.value = sessionPlainText(session) || latestText || '';
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 = latestText || '';
});
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);
@@ -5620,6 +5644,21 @@ mod tests {
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onAcceptDisk"));
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onKeepCurrent"));
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onOpenDiff"));
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS
.contains("function populateSessionConflictDiffPanel"));
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS
.contains("data-testid', 'mnote-conflict-current-text"));
assert!(
DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("data-testid', 'mnote-conflict-disk-text")
);
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS
.contains("data-testid', 'mnote-conflict-merge-use-current"));
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS
.contains("data-testid', 'mnote-conflict-merge-use-disk"));
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("currentMergeText"));
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onUseCurrent"));
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onUseDisk"));
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onSaveMerge"));
}
#[tokio::test]