GSoC 2026 Proposal Draft - Idea 10: Automatic Conflict Resolution – Sriram Varun Kumar

Title: GSoC 2026 Proposal Draft - Idea 9: Automatic Conflict Resolution - Sriram Varun Kumar


Links:

  • GitHub profile: https://github.com/varunkumar-22

  • Forum introduction post: Introducing Varun Kumar

  • Pull requests submitted to Joplin:

    Merged PRs - 8

    #14504 :- Mobile:Rich Text Editor: Fix extra blank line above nested lists
    #14477:- Desktop: Fix UI freeze when closing plugin dialog with Escape key
    #14421:-All: Fixes #14335: Support include_deleted parameter for GET /folders endpoint
    #14461:-CLI: Fixes #13158: Fix null crash in e2ee decrypt command
    #14541:-Desktop: Fixes #14196: Fix file:// links with backslashes for Windows UNC paths
    #14580:-Mobile: Fix tapping rendered image scrolling to cursor position
    #14559:-CLI: Fix trailing spaces in ls -l output
    #14432:-Desktop: Fix "Copy dev mode command" producing an unquotable path on Windows


1. Introduction

I am Sriram Varun Kumar, a B.Tech Computer Science Engineering student with a strong interest in open source tooling and developer infrastructure. I have been using Joplin across my laptop and phone for over a year and have hit sync conflicts myself, which is what drew me to this project in the first place.

My background is primarily in TypeScript and React, and I have contributed to large open source codebases before. For this proposal I have spent time reading through the synchronizer code, tracing the full conflict handling path, and understanding how the revision system stores and reconstructs note history. I have also built a working prototype of the conflict resolution UI inside the actual Joplin desktop app as part of my preparation.


2. Project Summary

What problem it solves

Joplin is an open source, offline-first note taking app that lets you edit notes even when you’re not connected to the internet. This is one of its best features, but it also creates a real problem that a lot of users run into.

When you edit the same note on two different devices before either of them gets a chance to sync, Joplin ends up with two different versions of that note and no way to know which one is right. Right now when this happens, Joplin saves the local version as a conflict note with is_conflict = 1 and overwrites it with the remote version. The user is left to open both versions, read through them carefully, and manually figure out what to keep. For a short note this is just annoying. For a long note with alot of edits, especially on a phone, most people just give up and pick whichever version looks more complete.

The frustrating part is that most of these conflicts aren’t actually conflicts. If you added a paragraph at the top of a note on your laptop and your phone added a sentence at the bottom, those two edits don’t touch each other at all. There is no reason a human should have to manually resolve that. This is exactly the problem that tools like Git solved a long time ago using three-way merge algorithms.

What will be implemented

A conflict resolution UI inside Joplin built on top of the existing conflict note mechanism. When a conflict happens the existing behaviour stays completely unchanged , the local version goes to the Conflicts folder and the remote version overwrites the original note. The difference is what happens when the user opens a conflict note.

Instead of seeing a raw copy with no context, a proper resolution UI opens automatically showing what each side changed. The UI uses the local version from the Conflicts folder, the remote version from the original note, and the base version from sync_items to show a clear diff. The user resolves each section by choosing which version to keep.

When all sections are resolved and the user clicks Finish, the merged result is written to the original note, the conflict note is deleted, and base_body, base_title and base_conflict_note_id in sync_items are updated explicitly. Sync is never blocked at any point.

Expected outcome

For all conflicts the experience will be significantly better than what it is today. Instead of opening two raw note copies and figuring out what to keep manually, the user gets a clear view of what each side changed and resolves it section by section. The reason for not fully automating the resolution is to avoid any risk of data loss for notes that may contain non plain text or encrypted data.


3. Technical Approach

Architecture and components involved

The implementation touches three layers. The diff module lives in a new platform-independent module at packages/lib/services/conflict/mergeNotes.ts with no UI dependencies. The synchronizer addition hooks into Synchronizer.ts at the upload step to store the base snapshot. The UI lives in packages/app-desktop/gui/ConflictResolution/ for desktop and packages/app-mobile/components/ConflictResolution/ for mobile.

The diff module uses node-diff3 to compare all three versions line by line. A section is classified as an addition or deletion when the diff shows a change on only one side relative to the base. A section is classified as a genuine conflict when both sides differ from the base in different ways on the same lines. For two-way diff, every difference is treated as a genuine conflict since there is no base to determine which side changed.

How conflicts work today

The conflict detection logic is spread across three files.

  1. packages/lib/Synchronizer.ts is where conflicts are first detected. During the upload step it compares remoteContent.updated_time against local.sync_time. If the remote version is newer than the last time this device synced, Joplin flags it as a conflict.
  2. packages/lib/services/synchronizer/utils/handleConflictAction.ts takes over from there. It looks at what type of conflict it is, whether its a NoteConflict, ItemConflict or ResourceConflict, and routes it to the right handler.
  3. packages/lib/models/Note.ts has two key functions. mustHandleConflict() checks whether the conflict actually affects the title or body, and also checks for encryption. If its only a metadata change, the remote version wins silently. If the title or body is different, createConflictNote() saves the local version as a conflict note with is_conflict = 1 and lets remote overwrite the note.

At no point does Joplin try to merge the two versions. Even if the local device only edited paragraph 1 and the remote device only edited paragraph 10, Joplin treats it as a full conflict and saves a conflict note. The user is left with no help, no summary, and no merging at all.

Changes to the Joplin codebase

Sync pipeline changes

The change is minimal. The existing conflict detection and conflict note creation in handleConflictAction.ts stays completely unchanged. The only additions are:

  • Writing base_body, base_title, and base_conflict_note_id to sync_items after every clean upload in Synchronizer.ts

  • Writing base_conflict_note_id to sync_items at the point a conflict note is created in handleConflictAction.ts

This snapshot is only written when the upload completes with no conflict, or when the user explicitly finishes resolving a conflict. It gives the conflict resolution UI a common ancestor to diff against when the user opens a conflict note later.

Conflict behaviour

When a conflict is detected, Joplin continues to do exactly what it does today:

  • The local version is saved as a conflict note with is_conflict = 1 in the Conflicts folder

  • The remote version overwrites the original note

The difference is what happens next. Instead of leaving the user to open both notes manually, opening the conflict note in the Conflicts folder now automatically shows the conflict resolution UI.

Three-way vs two-way diff

A base_conflict_note_id column is added to sync_items to determine whether a three-way diff is possible. This is set to the conflict note ID at the point the conflict note is created. When the conflict UI opens:

  • Match :- base_conflict_note_id matches the current conflict note ID → three-way diff using base_body as the common ancestor

  • No match :- a second conflict happened on the same note before the first was resolved, making the base stale → falls back to a two-way diff between the conflict note and the original note

  • Empty base_body :- note was synced before this feature was deployed → UI still opens but shows a two-way diff. No data is ever lost.

Three version sources

  • Base - the version stored in sync_items at the last clean upload

  • Local - the conflict note in the Conflicts folder

  • Remote - the current content of the original note

The UI uses all three to show the user exactly what each side changed so they can make an informed decision for each section.

Storing unresolved conflicts

When the user makes their first resolution action, four new columns on the conflict note are populated:

  • conflict_base_body

  • conflict_base_title

  • conflict_remote_body

  • conflict_remote_title

For three-way diff, all four columns are populated with copies of the base and remote versions. For two-way diff, only conflict_remote_body and conflict_remote_title are populated since there is no base available.

The conflict note body and title remain unchanged throughout resolution. They represent the local version and are only written to the database in two cases:

  • When the user clicks Done in the Edit Manually flow

  • When the Finish button is clicked to complete the merge

As the user resolves each section, the relevant sections are updated to match across all stored versions so the section disappears from the UI. When using Edit Manually, the user types directly into the conflict note body or title and the database is not updated until the user clicks Done.

When any of these four columns are populated, the UI knows that resolution has been started but not finished. After each resolution action the diff is re-evaluated. When no sections remain, the conflict_ columns are cleared and the Finish button becomes enabled. This approach works identically for body and title conflicts, and for both three-way and two-way diff.

Determining the diff mode:
Once resolution has started and the conflict_ columns are populated, they become the source of truth for determining diff mode. This overrides the base_conflict_note_id check from sync_items which only applies on a fresh open before any resolution action has been taken.

The UI checks the columns to decide which diff mode to use:

  • If all four columns are populated, three-way diff is used

  • If only the remote columns are populated, two-way diff is used and every difference is treated as a conflict since there is no base to compare against

For two-way diff, since there is no base version available, the UI cannot reliably distinguish additions and deletions from genuine conflicts. All differences are therefore shown as genuine conflicts with Use Mine, Use Theirs and Edit Manually options. This is a more conservative approach that ensures no change is silently accepted.

Undo and redo

The undo and redo actions in the conflict UI are dedicated buttons separate from the editor undo/redo. They undo and redo full resolution steps - each undo reverses the last section resolution action and each redo reapplies it. The state is stored as diffs rather than full note contents to avoid memory issues with large notes.The editor has its own separate undo/redo state when the user is in Edit Manually mode.

Title conflicts

Title conflicts follow the same logic as body sections:

  • If only one side changed the title the UI shows Accept or Reject options

  • If both sides changed the title to different values the UI shows Use Mine, Use Theirs and Edit Manually options

This keeps the title and body handling consistent throughout.

Handling stale data

The stored remote and base fields can go stale in two situations:

  • The original note is updated while resolution is in progress

  • A second conflict for the same note completes while the first is still open

This is handled by a conflict_remote_updated_time column on the conflict note, which is set to the updated_time of the original note at the point the conflict is created. When the user opens a conflict note, the UI checks whether conflict_remote_updated_time still matches the current updated_time of the original note:

  • If it matches, resolution continues normally

  • If it does not match, the stored fields are cleared, conflict_remote_updated_time is updated to the new value, and the diff is recomputed using the latest remote version

Any partial resolution progress is lost in this case, but this is the right tradeoff since the alternative is silently losing changes made to the remote note. When this reset happens, a simple informational popup appears telling the user that the partial resolution state has been reset because the original note has been updated. This check only runs when the user opens or switches to a conflict note, not in real time, so the user is never interrupted mid-resolution.

Conflict resolution UI for desktop

Opening a conflict note in the Conflicts folder automatically shows the conflict resolution UI instead of the normal note editor. The UI shows the note title at the top with a clear label showing it is a conflict. Below that the diff is displayed section by section showing what each side changed.

Section types

All sections are shown to the user in the conflict resolution UI. There are two types of sections:

Additions and deletions :- sections where only one side made a change:

  • Both the local and remote versions of that section are shown so it is clear which side has the change and what the other side looks like

  • Shown with Accept and Reject buttons

  • There is no real conflict here since only one side changed, so the user just decides whether to keep the change or not

Genuine conflicts :- sections where both sides changed the same part:

  • All versions are displayed depending on the merge type - for a three-way merge, this includes the base version showing what the note looked like before either side changed it, the local version labelled Mine, and the remote version labelled Theirs. For a two-way merge where no common base is available, only the local and remote versions are shown.

  • This gives the user full context to make an informed decision before choosing

  • Shown with a red Conflict heading to make them stand out from the rest

  • Have Use Mine, Use Theirs, and Edit Manually options

  • Clicking Edit Manually opens a text area and the user clicks Done when finished to acknowledge that section.

Three bulk action buttons are available at any time:

  • Apply non conflicting changes :- accepts all additions and deletions in one click, leaving only the genuine conflicts for manual resolution

  • Apply remaining mine :- accepts all remaining unresolved sections using the local version

  • Apply remaining theirs :- accepts all remaining unresolved sections using the remote version

A Keep both button is also available. This works like Apply remaining mine but instead of replacing the original note it creates a new note in the same notebook with a distinguishing suffix on the title, preserving both copies. This ensures users who want to keep both versions can still do so, which is currently possible with the existing conflict note system. Since this action cannot be undone with the undo stack a confirmation dialog appears before it runs.

Finish button

  • Always visible but disabled until all sections have been acknowledged

  • Once every section is resolved the Finish button becomes enabled

  • Since the button is disabled until everything is acknowledged there is no risk of accidentally completing the merge - no confirmation dialog is needed

How each option marks a section as acknowledged:

  • Use Mine and Use Theirs - mark the section as acknowledged immediately on click

  • Edit Manually - opens a text area for the user to write their own version, with a Done button to confirm. Clicking Done marks that section as acknowledged and counts towards enabling the Finish button

Completion flow

When the user clicks Finish:

  • The merged result is written to the original note

  • The conflict note is deleted from the Conflicts folder

  • base_body, base_title and base_conflict_note_id in sync_items are updated explicitly

  • If more conflict notes exist the UI navigates to the next one

  • If none remain the user is taken back to the original note that was just resolved

Implementation note

  • The UI is a separate React component and does not touch the existing markdown editor, rich text editor, or any other editor type

  • It replaces the note viewer entirely when a conflict note is opened

  • Optionally a banner on the original note can navigate the user directly to the conflict note as a stretch goal.

Conflict resolution UI for mobile

On mobile the same approach applies. Opening a conflict note in the Conflicts folder shows the conflict resolution UI instead of the normal note editor.

Key differences from desktop:

  • Stacked single-section layout for small screens instead of the side by side view

  • A navigation banner on the original note that takes the user directly to the conflict note is a stretch goal for both desktop and mobile.

The same shared diff module powers both platforms so the behaviour is always identical.

Potential Challenges

The most important detail to get right is updating base_body, base_title and base_conflict_note_id in sync_items when the user completes a merge. All three must be updated explicitly at that point since sync may not trigger again if the resolved note matches the remote version exactly.

For notes synced before this feature was deployed, base_body will be empty. In that case the resolution UI still opens but shows a two-way diff between the conflict note and the original note instead of a three-way diff. No data is ever lost.

There is an edge case where a second conflict happens on the same note before the first has been resolved. In that case base_body in sync_items gets updated and no longer matches the conflict note sitting in the Conflicts folder. The base_conflict_note_id column handles this:

  • If base_conflict_note_id matches the current conflict note ID, three-way diff is used

  • If it does not match, the UI falls back to two-way diff automatically

No data is ever lost in this case either.

The stale remote data case is handled by conflict_remote_updated_time. If the original note changes while a conflict is being resolved:

  • The partial progress is cleared on next open

  • The diff is recomputed from the latest remote version

  • An informational popup is shown so the user always knows why their progress was reset

This check only happens on open or switch, not in real time, so the user is never interrupted mid-resolution.

The concurrent editing case where a new sync arrives while the user has a conflict note open is non-essential to solve since the banner idea handles it well enough in practice. Adding the banner on both desktop and mobile is desirable if possible within the timeline and will be treated as a high priority stretch goal.

The conflict_ columns on the conflict note must be carefully excluded from sync so they are never uploaded to the sync target. These columns are local resolution state and have no meaning on other devices.

The conflict resolution UI needs to handle sections that are long or span multiple lines gracefully, particularly on mobile. Key considerations include:

  • Section display length - how much of a section to show in the conflict row before truncating

  • Truncation logic - where to cut off with an ellipsis, with care to avoid splitting mid-word

  • Inline diff highlighting - visually distinguishing what changed within a section, similar to GitHub PR diffs

  • Multi-line manual edit flow - how the edit experience works when a section spans several lines

These will be discussed with mentors during community bonding and finalized before implementation begins.

Implementation Plan

Community Bonding

  • Trace the full conflict handling path in depth and confirm integration points with mentors

  • Understand how is_conflict = 1 and conflict_original_id work in the existing mechanism

  • Extend the existing PoC prototype into a complete standalone demo on real note data

  • Understand how conflict notes in the Conflicts folder are currently displayed, confirm the integration point for opening the resolution UI when a conflict note is opened, and confirm concurrent editing edge case behaviour with mentors

Weeks 1 and 2

  • Implement the diff computation in mergeNotes.ts to compare all three versions and classify sections for display in the UI

  • Implement base_body and base_title snapshot writing in Synchronizer.ts upload step

  • Unit tests covering all diff and section classification scenarios

Weeks 3 and 4

  • Add base_body, base_title and base_conflict_note_id columns to sync_items via Joplin's existing migration system, and add conflict_base_body, conflict_base_title, conflict_remote_body, conflict_remote_title and conflict_remote_updated_time columns to the conflict note via the same migration system

  • Implement base_conflict_note_id being set at the point of conflict note creation in handleConflictAction.ts

  • Implement conflict_remote_updated_time being set at the point of conflict note creation in handleConflictAction.ts

  • Implement explicit base_body, base_title and base_conflict_note_id update when user completes resolution

  • Integration tests verifying all 11 existing conflict tests still pass

Weeks 5 and 6

  • Build ConflictResolution React component that opens automatically when a conflict note is opened in the Conflicts folder

  • Implement staleness check on open comparing conflict_remote_updated_time against the current updated_time of the original note. If they do not match, clear the conflict columns, update conflict_remote_updated_time and recompute the diff. Show an informational popup telling the user that the partial resolution state has been reset because the original note has been updated

  • Implement lazy population of conflict_base_body, conflict_base_title, conflict_remote_body and conflict_remote_title columns on the conflict note on first resolution action

  • Implement section resolution logic — updating conflict_ columns so resolved sections disappear from the UI

  • Implement two-way diff mode using only remote columns when base is unavailable, treating every difference as a conflict

  • Implement Finish button that is always visible but disabled until all sections are acknowledged

  • Implement Done button inside Edit Manually flow so the user can explicitly acknowledge when they have finished editing that section

  • Implement dedicated undo and redo buttons in the conflict UI storing diffs rather than full note contents, separate from the editor undo/redo state. Undoing a resolution marks that section as unresolved and disables the Finish button again, redoing marks it back as resolved

  • Implement Apply non conflicting changes, Apply remaining mine and Apply remaining theirs buttons

  • Implement Keep both button with confirmation dialog that creates a new note in the same notebook with a suffix on the title

  • Implement write-back flow: merged result to original note, conflict note deleted, base_title, base_body and base_conflict_note_id updated in sync_items

  • Test full end to end flow

Weeks 7 and 8

  • Delete vs edit conflicts, resource conflicts and backward compatibility

  • Performance testing with large notes, mentor feedback and refactor

First Evaluation

Conflict resolution UI working end to end on desktop. User can open a conflict note in the Conflicts folder, review all sections, resolve them, and complete the merge using the Finish button. The original note gets updated correctly and the conflict note is deleted. All 11 existing conflict tests still passing.

Weeks 9 and 10

  • Build ConflictResolution React Native component with stacked layout

  • Test on iOS and Android, verify shared diff module works identically

Weeks 11 and 12

  • End to end testing across desktop and mobile with multiple sync targets

  • Developer docs, update readme/apps/conflict.md for users

  • Backward compatibility verification

  • Stretch goal: navigation banner on original note for desktop and mobile

  • Stretch goal: AI assisted resolution

Final Assessment

  • Final testing, PR cleanup, documentation check, backward compatibility sign-off

5. Deliverables

  • mergeNotes.ts - platform-independent diff module powering the conflict UI, with full unit test suite

  • Modified handleConflictAction.ts - conflict note creation unchanged, sets base_conflict_note_id in sync_items at the point of conflict note creation

  • Schema migration adding base_body, base_title and base_conflict_note_id columns to sync_items, and conflict_base_body, conflict_base_title, conflict_remote_body, conflict_remote_title and conflict_remote_updated_time columns to the conflict note

  • ConflictResolution React component for desktop Opens automatically when a conflict note is opened in the Conflicts folder. Includes:

    • Side by side view with Accept and Reject for additions and deletions

    • Use Mine, Use Theirs and Edit Manually for genuine conflicts

    • Done button inside Edit Manually to acknowledge completion

    • Finish button disabled until all sections acknowledged

    • Dedicated undo and redo buttons separate from editor state, with Finish button updating on every undo and redo

    • Apply non conflicting changes, Apply remaining mine and Apply remaining theirs

    • Keep both button with confirmation dialog

    • Staleness check with informational popup when conflict_remote_updated_time does not match

    • Navigation banner on original note as stretch goal

  • ConflictResolution React Native component for mobile - stacked layout, same shared module as desktop, navigation banner as stretch goal

  • Integration tests Extends Synchronizer.conflicts.test.ts. Covers all 11 existing conflict tests still passing, staleness check behaviour, two-way diff fallback, and end to end resolution flow on desktop and mobile with multiple sync targets.

  • Developer documentation Covers diff computation, three version sources, conflict_ column lifecycle, two-way vs three-way diff decision logic, staleness check behaviour, and integration points.

  • Updated user documentation at readme/apps/conflict.md


6. Availability

  • 40 hours per week throughout the GSoC period

  • Timezone: IST (GMT+5:30), available 10am to 12 am IST

  • No exams, internships, or other commitments during the program

  • Will post weekly progress updates on the Joplin forum and remain active on Discord

  • Comfortable working asynchronously with mentors in different timezones


Hi @mrjo118 @CalebJohn @Daeraxa i shared a draft proposal above and would really appreciate any feedback when you have a moment ,happy to make any changes if required any

Thank you

3 way merge is definately the logical way to go, but you can’t rely on note history to determine the base version. First of all, there is no guarantee that the user has enabled note history, and second of all the revision collection is restricted to only producing revisions once every 10 minutes. So they’re not suitable for this purpose. You’ll need to find another way to obtain the base version of a note.

In order to obtain a base version, you will need to add some logic to the upload step of the synchronizer, to store a base version of a note somewhere in the local database at the point of uploading a change to the server (only when the upload could be completed, ie. there is no conflict or the conflict has been automatically resolved). I think the most logical place to store it would be a new column on the sync_items table.

I feel the suggested implementation may be too complicated. Do we really need this separate note_conflicts table?

1 Like

@mrjo118 Thank you both for the feedback

I have updated the proposal based on your suggestions. The main changes I made are:

For the base version issue that @mrjo118 pointed out, I removed the revision history approach completely. Instead the base is now stored directly in the sync pipeline by adding base_body and base_title columns to the existing sync_items table. The snapshot is only written when an upload completes cleanly, meaning no conflict or the conflict was automatically resolved. If no snapshot exists yet the system falls back to existing conflict note behaviour so nothing is lost.

kindly look into it, suggest if there are any changes required

@laurent Thank you for pointing out the complexity
I removed the separate table and to keep schema changes minimal, which is why I moved it to a conflict_data column on the notes table.

but after thinking about it more carefully I realised there is actually a problem with that approach. The notes table gets synced across devices, so any column on it gets uploaded to the sync target. Conflict resolution state is local only, it only matters on the device where the conflict happened. Storing it on the notes table risks it getting synced to other devices which does not make sense.

So I think the separate note_conflicts table is actually the right design for this reason it works the same way as sync_items, stays local, and gets cleared once everything is resolved.

at the same time, it also add somewhat complexity for adding a new table. can you share your thoughts on this?

Maybe the conflicts could be stored in the note itself, like in a git conflict, but with some UI on top to display things properly and allow the user to fix the conflict?

1 Like

That makes a lot of sense and thank you for the suggestion

the merge engine would write a git style conflict markers directly into note body for any sections it could not resolve automatically, and the UI will read those markers and display a proper resolution panel instead of showing the raw text and once the user resolves a section the markers get replaced with the chosen varsion

This makes the conflict state syncs naturally with the note body itself . I will update the proposal regarding this approach

I think the updated approach sounds reasonable. I have a couple of additional comments:

  1. The title field is rendered in a single line input, and if the title is conflicting, then the git style conflict markers wouldn’t work so well. While the title field technically supports line breaks, if you were to edit the title then it would strip out the line breaks (on the mobile app at least) and make the conflict unparsable. Do you have an idea for how to solve this?
  2. Can you add to the proposal how you intend to handle conflicts for items where the base_body and base_title are not yet populated?

Thank you for reviewing it!

1.regarding the title field i think it will be better to go with something like:

the sync_items table already has base_body and base_title columns being added for this feature, so one more column called conflict_title can be added there and sync_items is local only and never get syncs which means the conflict state stays on the device where the conflict happened, which is exactly where it should be

when both sides changed the title to different values, the local title stays in the title field unchanged and the remote title gets written to sync_items.conflict_title then the UI detects this and shows a small banner directly above the title field showing both versions with a keep mine and use theirs button

once the user picks one, the title field is updated if needed and conflict_title is cleared and if only one side changed the title the change is accepted automatically with no user interaction needed

Does this approach is okay to go with?

2. When base_body is empty or null, then it saves the local version as a conflict note with is_conflict = 1 and let the remote overwrite but the user just gets the old experience for that one conflict. the next time they sync successfully after that, the snapshot gets written and future conflicts on that note will use three way merge properly. I will update the proposal about this

2. When base_body is empty or null, then it saves the local version as a conflict note with is_conflict = 1 and let the remote overwrite but the user just gets the old experience for that one conflict. the next time they sync successfully after that, the snapshot gets written and future conflicts on that note will use three way merge properly. I will update the proposal about this

That sounds ok. So to clarify, if base_body is populated, then no conflict will be created in the conflicts notebook, but the conflict resolution ui will show in the place of the note editor for the note in its original location? There’s a couple of things that need considering:

  1. What will happen if the user is currently on and editing a note which conflicts as the sync triggers in the background? Will the new conflicts ui open immediately? On desktop, currently in such a scenario the content is overwritten in front of the user so it should be possible to refresh the view in front of you. On the mobile app however I’m not sure the note would automatically refresh in this scenario. There is an event along the lines of note_needs_refresh which you can emmit though which will trigger an immeditate refresh
  2. Will the conflict ui be built into the editor or separately replace the note viewer / editor on demand? If it uses the existing editor, you would need to cater for the markdown editor, legacy markdown editor, rich text editor, plain text editor, and view mode. So I suspect it is easier the uses a separate ui for this. You would also need a Done / Close button in order to exit this ui

the sync_items table already has base_body and base_title columns being added for this feature, so one more column called conflict_title can be added there and sync_items is local only and never get syncs which means the conflict state stays on the device where the conflict happened, which is exactly where it should be

Hmm I’m not sure about this. Wouldn’t it be better to put it on a new column on the note table instead (excluded from the server object), as the conflicting body is stored directly on the note table too?

Also, how does the app determine when it needs to show the new conflict ui? I think there may already by an is_conflict column on the note table but I’m not sure if that has some special usage such as excluding it from the sync? If the conflicting data is now stored on the main note, you would have to make sure the way you determine it is a conflict does not cause unwanted behaviour

Thank you for the detailed questions

To confirm your clarification yes that is correct like when base_body is populated no conflict note gets created in the conflicts notebook and the original note gets the merged content with conflict markers in the body where sections could not be auto-resolved, and the conflict banner appears at the top of the note in its original location and the user does not have to go hunting in the conflicts notebook at all

for question 1, on desktop i will emit note_needs_refresh immediately after the merge completes so the view refreshes and shows the conflict banner without the user having to do anything. For mobile I will use the same note_needs_refresh event you mentioned since it triggers an immediate refresh so the user might see the note content update briefly but they will not lose their local edits because the merge engine writes the merged result including their local changes into the note body before the refresh happens

For question 2, yes the conflict resolution panel is completely separate from the editor internals and it does not touch CodeMirror or TinyMCE at all. when the user clicks review conflicts from the banner, a separate react component opens that reads the note body directly, finds the conflict markers, and renders them in its own side by side view [I mentioned screenshots of this in the proposal] when the user resolves a section the chosen text gets written back to the note body and the markers will are removed there will be a done/close button to return to the normal note view , this avoids having to handle the markdown editor, legacy markdown editor, rich text editor, plain text editor and view mode separately

On conflict_title storage, I thought about your suggestion of putting it on the notes table but I think sync_items might be a good approach [ apologize if i missed anything] the reason is that excluding a column from the server object requires modifying the serialisation logic in BaseItem.serialize() which is a sensitive part of the codebase and have need to be done carefully

sync_items on the other hand is local only by design and nothing in it ever reaches the server, so there is zero risk of accidental sync with no serialisation changes needed at all and also I am already adding base_body and base_title to sync_items for this feature so conflict_title fits naturally alongside them in the same migration also title conflict state is also temporary local sync state rather than note content, so conceptually sync_items feels right; Please share your thoughts on this

For detecting when to show the conflict UI, after thinking through the alternatives I think the simplest and most good approach is to just scan the note body for conflict markers when a note is opened. like if the body contains <<<<<<< the banner shows,if not the normal editor shows. No new column needed for detection at all. The markers in the body are already the single source of truth for whether a conflict exists. maintaining a separate flag alongside them creates two things that have to stay in sync, and if they ever diverge like let’s say for example if the user manually edits the body the flag becomes wrong. parsing the body directly means nothing can get out of sync. the performance impact is also negligible since scanning a string for a substring is an extremely fast operation even on very large notes. This also means no serialisation changes are needed and there is no risk of accidentally syncing any conflict detection state, will this be a good approach to go?

Sorry I haven’t read your last post yet, but I need to point out a concern I have.

I've realised there is a conflict between the 'automatic / assisted conflict resolution' proposal and the 'encrypted notes' proposal. Using the suggestion of replacing the note body with combined content using git style conflict markers, if you do that then where it is an encrypted note, it's going to prevent the content from being able to be decrypted and it's not legible to resolve the conflict either. On the other hand, if you do automatic conflict resolution based on no changes to the same line, it's also going to corrupt an encrypted note because that logic for safe merging of changes does not apply.

I think aside from encrypted notes project, there's nothing stopping a user from encrypting note content with any external tool of their choosing, so automatic conflict resolution based on no changes to the same line could corrupt content in those notes regardless. Maybe we need to drop fully automated conflict resolution and always provide the option to accept the change?

Thank you for raising it, I have been thinking about the best way to handle this and I think a combination works better;

for Joplin end to end encrypted notes, mustHandleConflict() already catches these before the merge engine runs so they always fall back safely and for the local note encryption project being proposed separately, the merge engine can check for its flag the same way and fall back for those notes too

for the external encryption case where there is no detectable flag, I think the right default is to make automatic merging transparent rather than completely dropping it, when the merge engine auto-resolves sections it shows a lightweight notification saying something like "3 sections were merged automatically" with a review and dismiss buttons. here most of the users will just dismiss it and never think about it but if someone has externally encrypted content and the merge produces something wrong they will see that notification and can review before anything is silently lost

On top of that, for users who want complete control there would be a setting to always show the full conflict UI instead of auto merging. This is opt-in so it does not affect the default experience for anyone.

So the full behaviour would be three levels. joplin encrypted notes always fall back to existing behaviour. Normal notes auto merge with a transparent notification so nothing happens silently. And users who want full manual control can enable that via settings

Is this approach okay?

Yeah I’m not sure about that. Regarding the note encryption project, ideally you don’t want to create a dependency to it as both projects will be worked on concurrently, and it still would not solve notes encrypted with external tools anyway. There is actually an existing Secure Notes plugin for Joplin which allows encrypting notes, though with several limitations.

The problem with auto merging something which is potentially unsafe is that you then need to implement a mechanism to reverse the change, and you don’t really want to put a time limit on how long you have to be able to do that. So you might as well just make it so you have to accept the change at a point when you open the conflict and see the differences, as this would require less changes to the core schema to persist the relevant data.

It might be worth considering to keep the existing functionality in all cases to create conflicts in the conflicts folder, and make the ui to resolve the conflicts available when you open the notes in that folder.

Thank you for the suggestion, this really clarifies things, I think you are right and the approach you are suggesting is actually cleaner and safer

I will update the proposal to in this direction

So if you keep the existing conflict creation mechanism in place, I guess that would mean you don’t need to store a combined conflict anywhere? You should then have the local note (in the conflicts folder), the remote note (now replaced on the note in the original notebook) and the base version (in the sync_items table). It should provide everything you need for 3 way merge. When a merge is accepted, it should remove the note in the conflicts folder, update the base version on the sync_item explicitly (as the sync may not trigger if the result of conflict resolution is the same as the remote version), and put the result in the note in the original notebook. Then do something like go to the next conflicting note in the conflicts notebook, or to the original note which was just resolved if there are no conflicts remaining.

Thank you for clarifying the design part once again, it is really helpful and i have acknowledged it , updated the proposal please check it out and suggest if any changes are required

1 Like

Overall looks good to me and a well written proposal. Well done!

I just have some small comments for rewording:

Encrypted notes, pruned revisions, delete vs edit conflicts, resource conflicts, backward compatibility

The encrypted notes part should be out of scope now, so can be removed there. No fully automatic merge means you don’t need to worry about it anymore.

Regarding “pruned revisions“, what work would you need to do around this? I wouldn’t worry too much about data discarded by the resolving of conflicts, because the existing revision service, unless explicitly disabled, should keep an recent version of the local note before the merge, which is probably a decent enough safeguard, though not perfect.

For most users, most of the time, conflicts will just disappear silently. For the cases where a real conflict does exist, the experience will be significantly better then what it is today via UI.

Disappear silently doesn’t quite fit the description with the new design of needing to manually accept any change. But I think it is worth mentioning in this paragraph that the reason for not having a truly automated experience is to avoid possible data loss for non plain text data that may be contained within notes, especially encrypted data.

Thank you for the feedback.

sorry I forgot to update those areas after the design changed. I will remove the encrypted notes and pruned revisions points from potential challenges since both were tied to the old automatic merge approach and i will also reword the expected outcome section ‘disappear silently’ no longer fits the new design and I will mention that the reason for not fully automating is to avoid data loss for notes containing non plain text or encrypted data

I will update the proposal now

1 Like