Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 65 additions & 20 deletions assets/inject/renderer-inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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));
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 = () => {
Expand Down
17 changes: 17 additions & 0 deletions crates/codex-plus-core/tests/cdp_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down