@@ -1627,6 +1627,12 @@ async function performSync(sourceId) {
16271627
16281628 // Two-way sync with tombstone support
16291629 try {
1630+ // Snapshot user-modified bookmark IDs BEFORE the pull phase.
1631+ // This snapshot is passed to reorderLocalToMatchCloud so it only skips
1632+ // reordering for folders the USER rearranged, not folders where
1633+ // sync-driven creates/moves/deletes happened to trigger onMoved events.
1634+ const userModifiedIdsSnapshot = new Set ( locallyModifiedBookmarkIds ) ;
1635+
16301636 // Step 1: Get current local bookmarks and tombstones
16311637 const bookmarkTree = await browser . bookmarks . getTree ( ) ;
16321638 const localFlat = flattenBookmarkTree ( bookmarkTree ) ;
@@ -1782,7 +1788,7 @@ async function performSync(sourceId) {
17821788 // so earlier moves don't shift later targets).
17831789 isSyncDrivenChange = true ;
17841790 try {
1785- await reorderLocalToMatchCloud ( cloudBookmarks ) ;
1791+ await reorderLocalToMatchCloud ( cloudBookmarks , userModifiedIdsSnapshot ) ;
17861792 } catch ( reorderErr ) {
17871793 console . warn ( '[MarkSyncr] Reorder pass failed (non-fatal):' , reorderErr . message ) ;
17881794 } finally {
@@ -2340,44 +2346,29 @@ async function addCloudBookmarksToLocal(cloudItems) {
23402346 const targetFolderId = await getOrCreateFolderPath ( item . _folderPath , rootFolder . id ) ;
23412347
23422348 if ( item . type === 'folder' ) {
2343- // It's a folder entry - create the folder at the correct index
2344- // First check if folder already exists
2349+ // It's a folder entry - create the folder WITHOUT specifying index.
2350+ // Index-based positioning is handled by reorderLocalToMatchCloud after
2351+ // all items are created. Setting index here causes cascading shifts:
2352+ // each insertion at index N shifts all items at N+ to the right, so
2353+ // subsequent inserts land at the wrong position. When the cloud index
2354+ // exceeds the local folder's item count, the browser clamps it to the
2355+ // end — the main cause of "bookmarks end up at the END of toolbar."
23452356 const children = await browser . bookmarks . getChildren ( targetFolderId ) ;
23462357 const existingFolder = children . find ( ( c ) => ! c . url && c . title === item . title ) ;
23472358
23482359 if ( existingFolder ) {
2349- // Folder exists - check if it's at the correct index
2350- if ( typeof item . index === 'number' && existingFolder . index !== item . index ) {
2351- // Move to correct position
2352- try {
2353- await browser . bookmarks . move ( existingFolder . id , {
2354- parentId : targetFolderId ,
2355- index : item . index ,
2356- } ) ;
2357- console . log (
2358- `[MarkSyncr] Moved existing folder "${ item . title } " to index ${ item . index } `
2359- ) ;
2360- } catch ( moveErr ) {
2361- console . warn ( `[MarkSyncr] Failed to move folder "${ item . title } ":` , moveErr ) ;
2362- }
2363- }
2364- // Cache the folder ID
2360+ // Folder already exists - just cache it, reorder pass will fix position
23652361 const folderFullPath = item . _folderPath
23662362 ? `${ item . _folderPath } /${ item . title } `
23672363 : item . title ;
23682364 folderCache . set ( `${ rootFolder . id } :${ folderFullPath } ` , existingFolder . id ) ;
23692365 } else {
2370- // Create new folder at the correct index
2366+ // Create new folder without index — reorder pass will position it
23712367 const createOptions = {
23722368 parentId : targetFolderId ,
23732369 title : item . title || 'Untitled Folder' ,
23742370 } ;
23752371
2376- // Set index if available
2377- if ( typeof item . index === 'number' && item . index >= 0 ) {
2378- createOptions . index = item . index ;
2379- }
2380-
23812372 const newFolder = await browser . bookmarks . create ( createOptions ) ;
23822373 addedFolders ++ ;
23832374
@@ -2387,21 +2378,19 @@ async function addCloudBookmarksToLocal(cloudItems) {
23872378 : item . title ;
23882379 folderCache . set ( `${ rootFolder . id } :${ folderFullPath } ` , newFolder . id ) ;
23892380
2390- console . log ( `[MarkSyncr] Created folder "${ item . title } " at index ${ item . index } ` ) ;
2381+ console . log (
2382+ `[MarkSyncr] Created folder "${ item . title } " (reorder pass will set position)`
2383+ ) ;
23912384 }
23922385 } else if ( item . url ) {
2393- // It's a bookmark - create at the correct index
2386+ // It's a bookmark - create WITHOUT index.
2387+ // Reorder pass will position it correctly afterward.
23942388 const createOptions = {
23952389 parentId : targetFolderId ,
23962390 title : item . title ?? '' ,
23972391 url : item . url ,
23982392 } ;
23992393
2400- // Set index if available
2401- if ( typeof item . index === 'number' && item . index >= 0 ) {
2402- createOptions . index = item . index ;
2403- }
2404-
24052394 await browser . bookmarks . create ( createOptions ) ;
24062395 addedBookmarks ++ ;
24072396 }
@@ -2439,7 +2428,7 @@ async function addCloudBookmarksToLocal(cloudItems) {
24392428 *
24402429 * @param {Array } cloudBookmarks - Flat array of cloud bookmarks with folderPath and index
24412430 */
2442- async function reorderLocalToMatchCloud ( cloudBookmarks ) {
2431+ async function reorderLocalToMatchCloud ( cloudBookmarks , userModifiedIds = null ) {
24432432 // Group cloud items by their normalized folder path
24442433 const cloudByFolder = new Map ( ) ;
24452434 for ( const cb of cloudBookmarks ) {
@@ -2524,18 +2513,22 @@ async function reorderLocalToMatchCloud(cloudBookmarks) {
25242513
25252514 if ( children . length < 2 ) continue ;
25262515
2527- // Skip reordering only when the pattern of local modifications looks like
2528- // a local reorder. When a user rearranges bookmarks locally, multiple (often
2529- // all) siblings in the folder are marked as locally modified. If we reorder
2530- // to match cloud in that case, we overwrite the user's intended arrangement.
2531- // The local order will be pushed to cloud in the push step instead.
2516+ // Skip reordering only when the user manually rearranged bookmarks
2517+ // before this sync started. We use `userModifiedIds` — a snapshot of
2518+ // locallyModifiedBookmarkIds taken at the START of performSync, before
2519+ // any sync-driven creates/moves/deletes. This ensures that sync-driven
2520+ // changes (adds, folder moves, tombstone deletions) don't accidentally
2521+ // trigger the skip, which was the root cause of "bookmarks end up at
2522+ // the end of the toolbar" — the reorder pass was being skipped because
2523+ // sync-driven onMoved events polluted locallyModifiedBookmarkIds.
2524+ const idsToCheck = userModifiedIds || locallyModifiedBookmarkIds ;
25322525 const modifiedChildrenCount = children . reduce (
2533- ( count , child ) => ( locallyModifiedBookmarkIds . has ( child . id ) ? count + 1 : count ) ,
2526+ ( count , child ) => ( idsToCheck . has ( child . id ) ? count + 1 : count ) ,
25342527 0
25352528 ) ;
25362529 if ( modifiedChildrenCount > 1 ) {
25372530 console . log (
2538- `[MarkSyncr] 🏠 Skipping reorder for folder "${ folderPath } " — contains multiple locally modified bookmarks (${ modifiedChildrenCount } )`
2531+ `[MarkSyncr] 🏠 Skipping reorder for folder "${ folderPath } " — contains multiple user- modified bookmarks (${ modifiedChildrenCount } )`
25392532 ) ;
25402533 continue ;
25412534 }
@@ -2717,27 +2710,22 @@ async function updateLocalBookmarksFromCloud(bookmarksToUpdate) {
27172710 ) ;
27182711 }
27192712
2720- // Move to new folder or position if needed
2721- if ( folderChanged || indexChanged ) {
2713+ // Move to new folder if the folder changed. Index-only changes are
2714+ // handled by the reorderLocalToMatchCloud pass, which processes all
2715+ // children of each folder in one go (last-to-first) to avoid the
2716+ // cascading index shift problem caused by sequential moves.
2717+ if ( folderChanged ) {
27222718 const rootFolder = rootFolders [ cloudRootKey ] || rootFolders . other || rootFolders . toolbar ;
27232719
27242720 if ( rootFolder ) {
27252721 const targetFolderId = await getOrCreateFolderPath ( cloudRelativePath , rootFolder . id ) ;
27262722
2727- const moveOptions = { parentId : targetFolderId } ;
2728- if ( cloud . index !== undefined && cloud . index >= 0 ) {
2729- moveOptions . index = cloud . index ;
2730- }
2731-
2732- await browser . bookmarks . move ( local . id , moveOptions ) ;
2723+ // Move to new folder without specifying index — reorder pass will position it
2724+ await browser . bookmarks . move ( local . id , { parentId : targetFolderId } ) ;
27332725
2734- if ( folderChanged ) {
2735- console . log (
2736- `[MarkSyncr] Moved ${ cloud . url } from "${ local . folderPath } " to "${ cloud . folderPath } "`
2737- ) ;
2738- } else {
2739- console . log ( `[MarkSyncr] Repositioned ${ cloud . url } to index ${ cloud . index } ` ) ;
2740- }
2726+ console . log (
2727+ `[MarkSyncr] Moved ${ cloud . url } from "${ local . folderPath } " to "${ cloud . folderPath } " (reorder pass will set position)`
2728+ ) ;
27412729 }
27422730 }
27432731
0 commit comments