diff --git a/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md b/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md index fc38ed29..81ffcbb3 100644 --- a/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md +++ b/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md @@ -87,3 +87,7 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - 修改:`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`。 - 未完成:`uploadFileToMediaAsset` 编排壳、`uploadFilesWithResolvedTarget`、conflict merge / accept / keep 业务逻辑、secondary pane conflict smoke 仍未迁出。 +- 2026-05-25:Codex 主控完成 document conflict panel 第二刀:`document-conflict-panel-runtime.js` 新增并导出 `createSessionConflictPanel(session, message, deps)`,只负责冲突面板 DOM 创建和按钮回调 wiring;`web_shell.rs` 仍持有 `acceptDiskVersion`、`keepCurrentEditorVersion`、`openConflictDiffPanel` 等业务逻辑,并保留完整 inline fallback。 + - 修改:`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 全链路完成。 diff --git a/rust/crates/mnote-web/browser/document-conflict-panel-runtime.js b/rust/crates/mnote-web/browser/document-conflict-panel-runtime.js index f2c1cb08..9118c420 100644 --- a/rust/crates/mnote-web/browser/document-conflict-panel-runtime.js +++ b/rust/crates/mnote-web/browser/document-conflict-panel-runtime.js @@ -23,6 +23,65 @@ function clearSessionConflictSurface(session, deps) { }); } +function createSessionConflictPanel(session, message, deps) { + deps = deps || {}; + var externalConflictMessage = String(deps.externalConflictMessage || '').trim() || '文件已在外部修改'; + var panel = document.createElement('section'); + panel.className = 'mnote-editor-conflict-panel'; + panel.setAttribute('data-testid', 'mnote-editor-conflict-panel'); + panel.setAttribute('role', 'status'); + panel.setAttribute('aria-live', 'polite'); + + var heading = document.createElement('h2'); + heading.textContent = '文件冲突'; + var text = document.createElement('p'); + text.textContent = message || externalConflictMessage; + var meta = document.createElement('div'); + meta.className = 'mnote-conflict-meta'; + var fileLabel = session && session.sessionKind === 'resource' + ? String(session.rootUri || '') + '/' + String(session.resourcePath || session.documentId || '') + : String(session && (session.rootUri || session.documentId) || ''); + var sourceLabel = typeof deps.conflictSourceLabel === 'function' + ? deps.conflictSourceLabel(session) + : ''; + meta.textContent = '文件:' + fileLabel + ' · 来源:' + (sourceLabel || '本地文件变更'); + + var actions = document.createElement('div'); + actions.className = 'mnote-conflict-actions'; + var acceptDisk = document.createElement('button'); + acceptDisk.type = 'button'; + acceptDisk.textContent = '接受磁盘版本'; + acceptDisk.setAttribute('data-testid', 'mnote-conflict-accept-disk'); + var keepCurrent = document.createElement('button'); + keepCurrent.type = 'button'; + keepCurrent.textContent = '保留当前编辑器版本'; + keepCurrent.setAttribute('data-testid', 'mnote-conflict-keep-current'); + var openDiff = document.createElement('button'); + openDiff.type = 'button'; + openDiff.textContent = '打开 diff'; + openDiff.setAttribute('data-testid', 'mnote-conflict-open-diff'); + actions.append(acceptDisk, keepCurrent, openDiff); + + var diffPanel = document.createElement('div'); + diffPanel.className = 'mnote-conflict-diff-panel'; + diffPanel.setAttribute('data-testid', 'mnote-conflict-diff-panel'); + diffPanel.hidden = true; + panel.append(heading, text, meta, actions, diffPanel); + + acceptDisk.addEventListener('click', function() { + if (typeof deps.onAcceptDisk === 'function') deps.onAcceptDisk(session, panel); + }); + keepCurrent.addEventListener('click', function() { + if (typeof deps.onKeepCurrent === 'function') deps.onKeepCurrent(session, panel); + }); + openDiff.addEventListener('click', function() { + if (typeof deps.onOpenDiff === 'function') deps.onOpenDiff(session, panel); + }); + + return panel; +} + window.__mnoteDocumentConflictPanelRuntime = { - clearSessionConflictSurface: clearSessionConflictSurface + clearSessionConflictSurface: clearSessionConflictSurface, + createSessionConflictPanel: createSessionConflictPanel }; diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index eb87c473..59ad789d 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -2103,58 +2103,83 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { sessionViews(session).forEach((view) => { const host = view.runtimeDescriptor.root.closest('.document-pane') || view.runtimeDescriptor.root; if (!(host instanceof HTMLElement)) return; - const panel = document.createElement('section'); - panel.className = 'mnote-editor-conflict-panel'; - panel.setAttribute('data-testid', 'mnote-editor-conflict-panel'); - panel.setAttribute('role', 'status'); - panel.setAttribute('aria-live', 'polite'); - - const heading = document.createElement('h2'); - heading.textContent = '文件冲突'; - const text = document.createElement('p'); - text.textContent = message || externalConflictMessage; - const meta = document.createElement('div'); - meta.className = 'mnote-conflict-meta'; - const fileLabel = session.sessionKind === 'resource' - ? `${session.rootUri || ''}/${session.resourcePath || session.documentId}` - : (session.rootUri || session.documentId); - meta.textContent = `文件:${fileLabel} · 来源:${conflictSourceLabel(session) || '本地文件变更'}`; - const actions = document.createElement('div'); - actions.className = 'mnote-conflict-actions'; - const acceptDisk = document.createElement('button'); - acceptDisk.type = 'button'; - acceptDisk.textContent = '接受磁盘版本'; - acceptDisk.setAttribute('data-testid', 'mnote-conflict-accept-disk'); - const keepCurrent = document.createElement('button'); - keepCurrent.type = 'button'; - keepCurrent.textContent = '保留当前编辑器版本'; - keepCurrent.setAttribute('data-testid', 'mnote-conflict-keep-current'); - const openDiff = document.createElement('button'); - openDiff.type = 'button'; - openDiff.textContent = '打开 diff'; - openDiff.setAttribute('data-testid', 'mnote-conflict-open-diff'); - actions.append(acceptDisk, keepCurrent, openDiff); - const diffPanel = document.createElement('div'); - diffPanel.className = 'mnote-conflict-diff-panel'; - diffPanel.setAttribute('data-testid', 'mnote-conflict-diff-panel'); - diffPanel.hidden = true; - panel.append(heading, text, meta, actions, diffPanel); - - acceptDisk.addEventListener('click', () => { - acceptDiskVersion(session).catch((error) => { - setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error)); - renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error)); + const runtime = window.__mnoteDocumentConflictPanelRuntime; + let panel = null; + if (runtime && typeof runtime.createSessionConflictPanel === 'function') { + panel = runtime.createSessionConflictPanel(session, message, { + externalConflictMessage, + conflictSourceLabel, + onAcceptDisk: () => { + acceptDiskVersion(session).catch((error) => { + setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error)); + renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error)); + }); + }, + onKeepCurrent: () => { + keepCurrentEditorVersion(session).catch((error) => { + setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error)); + renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error)); + }); + }, + onOpenDiff: (_session, createdPanel) => { + openConflictDiffPanel(session, createdPanel); + }, }); - }); - keepCurrent.addEventListener('click', () => { - keepCurrentEditorVersion(session).catch((error) => { - setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error)); - renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error)); + } + if (!(panel instanceof HTMLElement)) { + panel = document.createElement('section'); + panel.className = 'mnote-editor-conflict-panel'; + panel.setAttribute('data-testid', 'mnote-editor-conflict-panel'); + panel.setAttribute('role', 'status'); + panel.setAttribute('aria-live', 'polite'); + + const heading = document.createElement('h2'); + heading.textContent = '文件冲突'; + const text = document.createElement('p'); + text.textContent = message || externalConflictMessage; + const meta = document.createElement('div'); + meta.className = 'mnote-conflict-meta'; + const fileLabel = session.sessionKind === 'resource' + ? `${session.rootUri || ''}/${session.resourcePath || session.documentId}` + : (session.rootUri || session.documentId); + meta.textContent = `文件:${fileLabel} · 来源:${conflictSourceLabel(session) || '本地文件变更'}`; + const actions = document.createElement('div'); + actions.className = 'mnote-conflict-actions'; + const acceptDisk = document.createElement('button'); + acceptDisk.type = 'button'; + acceptDisk.textContent = '接受磁盘版本'; + acceptDisk.setAttribute('data-testid', 'mnote-conflict-accept-disk'); + const keepCurrent = document.createElement('button'); + keepCurrent.type = 'button'; + keepCurrent.textContent = '保留当前编辑器版本'; + keepCurrent.setAttribute('data-testid', 'mnote-conflict-keep-current'); + const openDiff = document.createElement('button'); + openDiff.type = 'button'; + openDiff.textContent = '打开 diff'; + openDiff.setAttribute('data-testid', 'mnote-conflict-open-diff'); + actions.append(acceptDisk, keepCurrent, openDiff); + const diffPanel = document.createElement('div'); + diffPanel.className = 'mnote-conflict-diff-panel'; + diffPanel.setAttribute('data-testid', 'mnote-conflict-diff-panel'); + diffPanel.hidden = true; + panel.append(heading, text, meta, actions, diffPanel); + + acceptDisk.addEventListener('click', () => { + acceptDiskVersion(session).catch((error) => { + setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error)); + renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error)); + }); }); - }); - openDiff.addEventListener('click', () => { - openConflictDiffPanel(session, panel); - }); + keepCurrent.addEventListener('click', () => { + keepCurrentEditorVersion(session).catch((error) => { + setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error)); + renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error)); + }); + }); + openDiff.addEventListener('click', () => { + openConflictDiffPanel(session, panel); + }); + } const header = host.querySelector('.document-shell-header'); if (header && header.parentNode) { @@ -5579,6 +5604,24 @@ mod tests { )); } + #[test] + fn document_conflict_panel_runtime_contains_dom_helpers() { + const DOCUMENT_CONFLICT_PANEL_RUNTIME_JS: &str = + include_str!("../../browser/document-conflict-panel-runtime.js"); + assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("function clearSessionConflictSurface")); + assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("function createSessionConflictPanel")); + assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS + .contains("data-testid', 'mnote-editor-conflict-panel")); + assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS + .contains("data-testid', 'mnote-conflict-accept-disk")); + assert!( + DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("data-testid', 'mnote-conflict-open-diff") + ); + assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onAcceptDisk")); + assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onKeepCurrent")); + assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onOpenDiff")); + } + #[tokio::test] async fn sidebar_and_filetree_do_not_return_dev_fixtures_by_default() { let config = AppConfig {