diff --git a/.gitattributes b/.gitattributes index 561487b9d..2e47b8a64 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,10 +7,7 @@ *.sh text eol=lf .github/workflows/*.yml text eol=lf -# Preserve the original Snow Skin asset line endings for byte-exact bundling. -assets/inject/upstream/snow-skin/*.js text eol=lf -assets/inject/upstream/snow-skin/*.css text eol=lf - -# Keep byte-exact macOS theme assets identical on every checkout platform. -assets/inject/upstream/*/macos/*.js text eol=lf -assets/inject/upstream/*/macos/*.css text eol=lf +# Keep every byte-exact upstream theme asset stable on all checkout platforms. +assets/inject/upstream/**/*.js text eol=lf +assets/inject/upstream/**/*.css text eol=lf +assets/inject/upstream/skin-packs/packs/*/theme.json text eol=lf diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index fda3287e9..17917341e 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -7478,11 +7478,23 @@ async function refreshRecentConversationsForHost() { try { - const signals = await import("./assets/app-server-manager-signals-C1h8B-R-.js"); - if (typeof signals.rn === "function") await signals.rn("refresh-recent-conversations-for-host", { hostId: "local", sortKey: "updated_at" }); + const signals = await loadOptionalCodexAppModule("app-server-manager-signals-"); + const sendRequest = Object.values(signals || {}).find((candidate) => { + if (typeof candidate !== "function") return false; + try { + const source = Function.prototype.toString.call(candidate).replace(/\s+/g, ""); + return /^function[$\w]+\(e,t\)\{return[$\w]+\.sendRequest\(e,t\)\}$/.test(source); + } catch { + return false; + } + }); + if (typeof sendRequest !== "function") return false; + await sendRequest("refresh-recent-conversations-for-host", { hostId: "local", sortKey: "updated_at" }); + return true; } catch (error) { window.__codexProjectMoveRefreshFailures = window.__codexProjectMoveRefreshFailures || []; window.__codexProjectMoveRefreshFailures.push(String(error?.stack || error)); + return false; } } @@ -7677,7 +7689,10 @@ undo.addEventListener("click", async () => { const result = await postJson("/undo", { undo_token: undoToken }); toast.textContent = result.message || "撤销完成"; - if (result.status === "undone") window.location.reload(); + if (result.status === "undone") { + const refreshed = await refreshRecentConversationsForHost(); + if (!refreshed) window.location.reload(); + } setTimeout(() => toast.remove(), 5000); }); toast.appendChild(undo); @@ -8874,7 +8889,6 @@ } function installDeleteButtonEventDelegation() { - document.removeEventListener("pointerup", window.__codexSessionDeleteDocumentDeleteHandler, true); document.removeEventListener("click", window.__codexSessionDeleteDocumentDeleteHandler, true); const handler = (event) => { const button = event.target?.closest?.(`.${buttonClass}`); @@ -8885,7 +8899,6 @@ openDeleteConfirmForRow(row, button, ref, event); }; window.__codexSessionDeleteDocumentDeleteHandler = handler; - document.addEventListener("pointerup", handler, true); document.addEventListener("click", handler, true); } @@ -8965,7 +8978,6 @@ button.addEventListener("pointerleave", hideActionButtonTooltip); button.addEventListener("focus", () => showActionButtonTooltip(button)); button.addEventListener("blur", hideActionButtonTooltip); - button.addEventListener("pointerup", onActivate, true); button.addEventListener("click", (event) => { hideActionButtonTooltip(); onActivate(event); diff --git a/crates/codex-plus-core/tests/cdp_bridge.rs b/crates/codex-plus-core/tests/cdp_bridge.rs index d988f23f5..d7e4889bd 100644 --- a/crates/codex-plus-core/tests/cdp_bridge.rs +++ b/crates/codex-plus-core/tests/cdp_bridge.rs @@ -1229,6 +1229,56 @@ fn injection_script_keeps_session_action_buttons_in_pr_style() { assert!(script.contains("cursor: default;")); } +#[test] +fn injection_script_activates_session_delete_once_per_click() { + let script = assets::injection_script(57321); + let delegated_delete = script + .split_once("function installDeleteButtonEventDelegation()") + .expect("delete event delegation should exist") + .1 + .split_once("function actionGroupFromRow") + .expect("delete event delegation should end before action group helpers") + .0; + let action_button_events = script + .split_once("function installActionButtonEvents") + .expect("action button event setup should exist") + .1 + .split_once("function installMoreButtonEvents") + .expect("action button setup should end before more button setup") + .0; + + assert!(delegated_delete.contains("document.addEventListener(\"click\", handler, true);")); + assert!(!delegated_delete.contains("document.addEventListener(\"pointerup\", handler, true);")); + assert!( + !action_button_events.contains("button.addEventListener(\"pointerup\", onActivate, true);") + ); + assert!(action_button_events.contains("button.addEventListener(\"click\", (event) => {")); +} + +#[test] +fn injection_script_refreshes_sidebar_after_session_undo() { + let script = assets::injection_script(57321); + let refresh = script + .split_once("async function refreshRecentConversationsForHost()") + .expect("recent conversation refresh helper should exist") + .1 + .split_once("function refreshAfterProjectMove") + .expect("refresh helper should end before project move refresh") + .0; + let toast = script + .split_once("function showToast(message, undoToken)") + .expect("undo toast should exist") + .1 + .split_once("function upstreamWorktreeField") + .expect("undo toast should end before worktree helpers") + .0; + + assert!(refresh.contains("loadOptionalCodexAppModule(\"app-server-manager-signals-\")")); + assert!(!refresh.contains("app-server-manager-signals-C1h8B-R-.js")); + assert!(toast.contains("const refreshed = await refreshRecentConversationsForHost();")); + assert!(toast.contains("if (!refreshed) window.location.reload();")); +} + #[test] fn injection_script_moves_export_and_project_move_into_more_menu() { let script = assets::injection_script(57321).replace("\r\n", "\n"); @@ -1431,6 +1481,19 @@ fn injection_script_discovers_app_server_request_clients_without_hardcoded_hash( ); } +#[test] +fn injection_script_refreshes_sidebar_after_undo_without_stale_asset_exports() { + let script = assets::injection_script(57321); + + assert!(script.contains("loadOptionalCodexAppModule(\"app-server-manager-signals-\")")); + assert!(script.contains("Object.values(signals || {}).find")); + assert!(script.contains("refresh-recent-conversations-for-host")); + assert!(script.contains("const refreshed = await refreshRecentConversationsForHost()")); + assert!(script.contains("if (!refreshed) window.location.reload()")); + assert!(!script.contains("app-server-manager-signals-C1h8B-R-.js")); + assert!(!script.contains("typeof signals.rn")); +} + #[test] fn injection_script_clears_project_state_when_moving_to_projectless() { let script = assets::injection_script(57321);