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.
- 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.
- 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.
- 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





