diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index 2cfec7715..5d5af207e 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -406,6 +406,7 @@ const projectMoveRefreshDelaysMs = [50, 250, 750, 1500]; const chatsSortRefreshIntervalMs = 1500; const chatsSortDbRefreshIntervalMs = 5000; + const provisionalThreadSortMs = new Map(); const styleId = "codex-delete-style"; const codexDeleteStyleVersion = "14"; const codexPlusMenuId = "codex-plus-menu"; @@ -6758,7 +6759,14 @@ } function sortMsForSession(sessionId, preferredValue) { - return numericTimestamp(preferredValue) || uuidV7TimestampMs(sessionId); + const preferredTimestamp = numericTimestamp(preferredValue); + if (preferredTimestamp) return preferredTimestamp; + const uuidTimestamp = uuidV7TimestampMs(sessionId); + if (uuidTimestamp) return uuidTimestamp; + const key = projectMoveSessionKey(sessionId); + if (!key.startsWith("client-new-thread:")) return 0; + if (!provisionalThreadSortMs.has(key)) provisionalThreadSortMs.set(key, Date.now()); + return provisionalThreadSortMs.get(key) || 0; } function timestampMsFromPayload(payload) { @@ -6990,6 +6998,27 @@ return chatsSection()?.querySelector?.('[role="list"][aria-label="对话"], [role="list"]') || null; } + function threadListForRow(row) { + return row?.closest?.('[data-app-action-sidebar-project-list-id], [data-codex-project-move-injected-list="true"], [role="list"]') || null; + } + + function visibleSidebarRows() { + return sessionRows(true).filter((row) => rowIsInChats(row) || !!closestProjectListItem(row)); + } + + function sidebarRowsByList(rows = visibleSidebarRows()) { + const groups = new Map(); + rows.forEach((row) => { + const list = threadListForRow(row); + if (!list) return; + const item = rowListItem(row); + const group = groups.get(list) || []; + if (!group.some((entry) => rowListItem(entry) === item)) group.push(row); + groups.set(list, group); + }); + return groups; + } + function rowIsUnderTargetProject(row, target) { const item = closestProjectListItem(row); return !!item && projectItemMatchesTarget(item, target); @@ -7385,7 +7414,7 @@ return Array.from(list.children).map(threadRowFromListItem).filter(Boolean).filter((row) => rowIsInChats(row)); } - function chatsSortNeedsCorrection(rows) { + function threadRowsNeedCorrection(rows) { let previousPinned = true; let previousSortMs = Infinity; let previousKey = "\uffff"; @@ -7408,12 +7437,8 @@ return false; } - function reorderChatsRows(rows) { - const list = chatsThreadList(); - if (!list || rows.length < 2) return; - const rowItems = new Set(rows.map(rowListItem)); - const firstNonThreadItem = Array.from(list.children).find((child) => !rowItems.has(child) && !threadRowFromListItem(child)); - const orderedRows = [...rows].sort((left, right) => { + function orderedThreadRows(rows) { + return [...rows].sort((left, right) => { const leftPinned = rowPinned(left); const rightPinned = rowPinned(right); if (leftPinned !== rightPinned) return leftPinned ? -1 : 1; @@ -7424,31 +7449,48 @@ if (leftSortMs !== rightSortMs) return rightSortMs - leftSortMs; return projectMoveSessionKey(rightRef.session_id).localeCompare(projectMoveSessionKey(leftRef.session_id)); }); + } + + function reorderThreadRows(list, rows) { + if (!list || rows.length < 2) return; + const rowItems = new Set(rows.map(rowListItem)); + const firstNonThreadItem = Array.from(list.children).find((child) => !rowItems.has(child) && !threadRowFromListItem(child)); + const orderedRows = orderedThreadRows(rows); orderedRows.forEach((row) => list.insertBefore(rowListItem(row), firstNonThreadItem || null)); cachedSessionRowsAt = 0; } + function sidebarSortSignature(groups = sidebarRowsByList()) { + return Array.from(groups.values()) + .map((rows) => rows.map((row) => projectMoveSessionKey(sessionRefFromRow(row).session_id)).join("|")) + .join("||"); + } + async function applyChatsSortCorrection() { if (!codexPlusSettings().projectMove || chatsSortInFlight) return; - const rows = visibleChatsRows(); + const rows = visibleSidebarRows(); if (rows.length < 2) return; + const groups = sidebarRowsByList(rows); const refs = rows.map(sessionRefFromRow).filter((ref) => ref.session_id); - const signature = refs.map((ref) => projectMoveSessionKey(ref.session_id)).join("|"); + const signature = sidebarSortSignature(groups); const allRowsHaveSortMs = rows.every((row) => numericTimestamp(row.dataset.codexProjectMoveSortMs || rowListItem(row).dataset.codexProjectMoveSortMs)); + const needsCorrection = Array.from(groups.values()).some((group) => threadRowsNeedCorrection(group)); const shouldRefreshSortKeys = signature !== chatsSortSignature || !allRowsHaveSortMs || Date.now() - chatsSortLastFetchAt > chatsSortDbRefreshIntervalMs; - if (!shouldRefreshSortKeys && !chatsSortNeedsCorrection(rows)) return; + if (!shouldRefreshSortKeys && !needsCorrection) return; chatsSortInFlight = true; try { if (shouldRefreshSortKeys) { - const result = await postJson("/thread-sort-keys", { sessions: refs }).catch(() => ({ status: "failed", sort_keys: [] })); - chatsSortLastFetchAt = Date.now(); const byId = new Map(); - if (result?.status === "ok" && Array.isArray(result?.sort_keys)) { - result.sort_keys.forEach((item) => { - const key = projectMoveSessionKey(String(item?.session_id || "")); - if (key) byId.set(key, item); - }); + for (let start = 0; start < refs.length; start += 200) { + const result = await postJson("/thread-sort-keys", { sessions: refs.slice(start, start + 200) }).catch(() => ({ status: "failed", sort_keys: [] })); + if (result?.status === "ok" && Array.isArray(result?.sort_keys)) { + result.sort_keys.forEach((item) => { + const key = projectMoveSessionKey(String(item?.session_id || "")); + if (key) byId.set(key, item); + }); + } } + chatsSortLastFetchAt = Date.now(); rows.forEach((row) => { const ref = sessionRefFromRow(row); const payload = byId.get(projectMoveSessionKey(ref.session_id)); @@ -7459,8 +7501,10 @@ if (trustedSortMs) updateRowTimeLabel(row, trustedSortMs); }); } - if (chatsSortNeedsCorrection(rows)) reorderChatsRows(rows); - chatsSortSignature = visibleChatsRows().map((row) => projectMoveSessionKey(sessionRefFromRow(row).session_id)).join("|"); + sidebarRowsByList(visibleSidebarRows()).forEach((group, list) => { + if (threadRowsNeedCorrection(group)) reorderThreadRows(list, group); + }); + chatsSortSignature = sidebarSortSignature(); } finally { chatsSortInFlight = false; } @@ -10326,6 +10370,7 @@ window.__codexProjectMoveReadProjection = readProjectMoveProjection; window.__codexProjectMoveTargets = projectMoveTargets; window.__codexProjectMoveSortChats = applyChatsSortCorrection; + window.__codexProjectMoveSortSidebar = applyChatsSortCorrection; window.removeEventListener("resize", window.__codexPlusResizeHandler); let codexPlusResizeRafId = 0; window.__codexPlusResizeHandler = () => { diff --git a/crates/codex-plus-core/tests/cdp_bridge.rs b/crates/codex-plus-core/tests/cdp_bridge.rs index 03025f736..c89dbc0ba 100644 --- a/crates/codex-plus-core/tests/cdp_bridge.rs +++ b/crates/codex-plus-core/tests/cdp_bridge.rs @@ -1514,6 +1514,23 @@ fn injection_script_moves_export_and_project_move_into_more_menu() { assert!(!script.contains("group.appendChild(moveButton)")); } +#[test] +fn injection_script_sorts_threads_within_each_sidebar_list() { + let script = assets::injection_script(57321); + + assert!(script.contains("const provisionalThreadSortMs = new Map()")); + assert!(script.contains("client-new-thread:")); + assert!(script.contains("function threadListForRow")); + assert!(script.contains("function visibleSidebarRows")); + assert!(script.contains("function sidebarRowsByList")); + assert!(script.contains("function threadRowsNeedCorrection")); + assert!(script.contains("function orderedThreadRows")); + assert!(script.contains("function reorderThreadRows")); + assert!(script.contains("function sidebarSortSignature")); + assert!(script.contains("refs.slice(start, start + 200)")); + assert!(script.contains("window.__codexProjectMoveSortSidebar")); +} + #[test] fn injection_script_does_not_add_delete_controls_on_archived_page() { let script = assets::injection_script(57321);