From 28af12ebc37f07e20626c7c8ae2731737467d9b9 Mon Sep 17 00:00:00 2001 From: Han Xi Date: Sun, 24 May 2026 09:56:31 +0800 Subject: [PATCH] Fix project move row alignment --- assets/inject/renderer-inject.js | 130 +++++++++++++++++++++ crates/codex-plus-core/tests/cdp_bridge.rs | 15 +++ 2 files changed, 145 insertions(+) diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index efcdeb6..5d138d7 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -3686,6 +3686,17 @@ return Array.from(row?.children || []).find((child) => String(child.className || "").includes("h-full w-full items-center")) || null; } + function rowTitleContainer(row) { + const title = row?.querySelector?.("[data-thread-title]"); + let current = title?.parentElement || null; + while (current && current !== row) { + const className = classNameText(current); + if (className.includes("flex") && className.includes("min-w-0") && className.includes("flex-1")) return current; + current = current.parentElement; + } + return null; + } + function normalizedText(node) { return String(node?.textContent || "").replace(/\s+/g, " ").trim(); } @@ -3872,6 +3883,123 @@ list.appendChild(item); } + function rememberProjectMoveOriginalLayout(row, item) { + if (item.dataset.codexProjectMoveOriginalItemClass === undefined) item.dataset.codexProjectMoveOriginalItemClass = item.getAttribute("class") || ""; + if (item.dataset.codexProjectMoveOriginalItemStyle === undefined) item.dataset.codexProjectMoveOriginalItemStyle = item.getAttribute("style") || ""; + if (row.dataset.codexProjectMoveOriginalRowClass === undefined) row.dataset.codexProjectMoveOriginalRowClass = row.getAttribute("class") || ""; + if (row.dataset.codexProjectMoveOriginalRowStyle === undefined) row.dataset.codexProjectMoveOriginalRowStyle = row.getAttribute("style") || ""; + } + + function threadRowsInList(list, excludedRow = null) { + return Array.from(list?.children || []) + .map(threadRowFromListItem) + .filter((candidate) => candidate && candidate !== excludedRow); + } + + function copyProjectMoveLayout(sourceRow, row, item) { + const sourceItem = rowListItem(sourceRow); + copyProjectMoveLayoutStyles(sourceItem, item); + copyProjectMoveLayoutStyles(sourceRow, row); + item.dataset.codexProjectMoveLayoutSource = "project"; + row.dataset.codexProjectMoveLayoutSource = "project"; + } + + function copyProjectMoveLayoutStyles(source, target) { + if (!source || !target || source === target) return; + const sourceStyle = window.getComputedStyle(source); + [ + "paddingLeft", + "paddingRight", + "paddingInlineStart", + "paddingInlineEnd", + "marginLeft", + "marginRight", + "marginInlineStart", + "marginInlineEnd", + ].forEach((property) => { + if (sourceStyle[property]) target.style[property] = sourceStyle[property]; + }); + ["--padding-row-x", "--padding-row-y"].forEach((property) => { + const value = sourceStyle.getPropertyValue(property); + if (value) target.style.setProperty(property, value); + }); + } + + function normalizeProjectThreadRowLayout(row, list, item = rowListItem(row)) { + if (!row || !item || !list) return; + rememberProjectMoveOriginalLayout(row, item); + const sourceRow = threadRowsInList(list, row).find((candidate) => !rowProjectionKind(candidate)); + if (sourceRow) copyProjectMoveLayout(sourceRow, row, item); + ensureProjectThreadIndent(row); + } + + function normalizeProjectlessThreadRowLayout(row, list, item = rowListItem(row)) { + if (!row || !item || !list) return; + rememberProjectMoveOriginalLayout(row, item); + removeProjectThreadIndent(row); + const sourceRow = threadRowsInList(list, row).find((candidate) => !rowProjectionKind(candidate)); + if (sourceRow) { + copyProjectMoveLayout(sourceRow, row, item); + item.dataset.codexProjectMoveLayoutSource = "projectless"; + row.dataset.codexProjectMoveLayoutSource = "projectless"; + return; + } + restoreProjectlessThreadRowLayout(row, item); + } + + function projectThreadIndentSpacer(row) { + const root = rowContentRoot(row); + const first = root?.firstElementChild; + if (first?.dataset?.codexProjectMoveIndentSpacer === "true") return first; + if (first && classNameText(first).split(/\s+/).includes("w-4") && !first.querySelector?.("[data-thread-title]")) return first; + return null; + } + + function ensureProjectThreadIndent(row) { + const root = rowContentRoot(row); + if (!root) return; + if (!projectThreadIndentSpacer(row)) { + const spacer = document.createElement("div"); + spacer.className = "w-4"; + spacer.dataset.codexProjectMoveIndentSpacer = "true"; + const inner = document.createElement("div"); + inner.className = "relative flex items-center justify-center"; + spacer.appendChild(inner); + root.insertBefore(spacer, root.firstChild); + } + const titleContainer = rowTitleContainer(row); + titleContainer?.classList?.add("ml-1.5"); + } + + function removeProjectThreadIndent(row) { + const spacer = projectThreadIndentSpacer(row); + if (spacer?.dataset?.codexProjectMoveIndentSpacer === "true") spacer.remove(); + rowTitleContainer(row)?.classList?.remove("ml-1.5"); + } + + function restoreProjectlessThreadRowLayout(row, item = rowListItem(row)) { + if (!row || !item) return; + removeProjectThreadIndent(row); + if (item.dataset.codexProjectMoveOriginalItemClass !== undefined) { + item.setAttribute("class", item.dataset.codexProjectMoveOriginalItemClass); + } + if (item.dataset.codexProjectMoveOriginalItemStyle !== undefined) { + const originalStyle = item.dataset.codexProjectMoveOriginalItemStyle; + if (originalStyle) item.setAttribute("style", originalStyle); + else item.removeAttribute("style"); + } + if (row.dataset.codexProjectMoveOriginalRowClass !== undefined) { + row.setAttribute("class", row.dataset.codexProjectMoveOriginalRowClass); + } + if (row.dataset.codexProjectMoveOriginalRowStyle !== undefined) { + const originalStyle = row.dataset.codexProjectMoveOriginalRowStyle; + if (originalStyle) row.setAttribute("style", originalStyle); + else row.removeAttribute("style"); + } + delete item.dataset.codexProjectMoveLayoutSource; + delete row.dataset.codexProjectMoveLayoutSource; + } + function projectMoveInjectedList(projectItem) { let list = projectItem.querySelector('[data-codex-project-move-injected-list="true"]'); if (!list) { @@ -3938,6 +4066,7 @@ const item = rowListItem(row); if (!list) return false; insertRowItemByTime(list, item, row, target); + normalizeProjectThreadRowLayout(row, list, item); cachedSessionRowsAt = 0; item.dataset.codexProjectMoveTargetKind = "project"; item.dataset.codexProjectMoveTargetCwd = targetPath(target); @@ -3952,6 +4081,7 @@ if (!list) return false; const item = rowListItem(row); insertRowItemByTime(list, item, row, target); + normalizeProjectlessThreadRowLayout(row, list, item); cachedSessionRowsAt = 0; item.dataset.codexProjectMoveTargetKind = "projectless"; row.dataset.codexProjectMoveTargetKind = "projectless"; diff --git a/crates/codex-plus-core/tests/cdp_bridge.rs b/crates/codex-plus-core/tests/cdp_bridge.rs index 87679c3..873ea70 100644 --- a/crates/codex-plus-core/tests/cdp_bridge.rs +++ b/crates/codex-plus-core/tests/cdp_bridge.rs @@ -162,6 +162,21 @@ fn injection_script_keeps_session_action_buttons_in_pr_style() { assert!(script.contains("cursor: default;")); } +#[test] +fn injection_script_normalizes_project_move_row_layout() { + let script = assets::injection_script(57321); + + assert!(script.contains("normalizeProjectThreadRowLayout")); + assert!(script.contains("restoreProjectlessThreadRowLayout")); + assert!(script.contains("ensureProjectThreadIndent(row);")); + assert!(script.contains("removeProjectThreadIndent(row);")); + assert!(script.contains("codexProjectMoveIndentSpacer")); + assert!(script.contains("codexProjectMoveOriginalItemClass")); + assert!(script.contains("codexProjectMoveLayoutSource")); + assert!(script.contains("normalizeProjectThreadRowLayout(row, list, item);")); + assert!(script.contains("restoreProjectlessThreadRowLayout(row, item);")); +} + #[test] fn injection_script_unlocks_custom_model_catalog() { let script = assets::injection_script(57321);