From be6c144ea83be7eed2bae832d77ed9146620c6f3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Apr 2026 02:56:17 +0000
Subject: [PATCH 1/2] feat: send desktop notifications on repo errors and
 conflicts

Agent-Logs-Url: https://github.com/cognitoforms/git-sync/sessions/80a246e7-607c-474b-a875-13ccb01838ac

Co-authored-by: michaelhthomas <18223295+michaelhthomas@users.noreply.github.com>
---
 Cargo.lock                          | 426 +++++++++++++++++++++++++++-
 src-tauri/Cargo.toml                |   9 +-
 src-tauri/capabilities/default.json |   1 +
 src-tauri/src/lib.rs                |  85 +++++-
 4 files changed, 514 insertions(+), 7 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 0145829..6d7559e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -62,6 +62,18 @@ dependencies = [
  "derive_arbitrary",
 ]
 
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "async-channel"
 version = "2.5.0"
@@ -74,6 +86,113 @@ dependencies = [
  "pin-project-lite",
 ]
 
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
 [[package]]
 name = "atk"
 version = "0.18.2"
@@ -169,6 +288,19 @@ dependencies = [
  "objc2",
 ]
 
+[[package]]
+name = "blocking"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
 [[package]]
 name = "brotli"
 version = "8.0.2"
@@ -784,6 +916,33 @@ version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
 
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
 [[package]]
 name = "equivalent"
 version = "1.0.2"
@@ -1019,6 +1178,19 @@ version = "0.3.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
 
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "futures-macro"
 version = "0.3.32"
@@ -1309,6 +1481,7 @@ dependencies = [
  "tauri",
  "tauri-build",
  "tauri-plugin-dialog",
+ "tauri-plugin-notification",
  "tauri-plugin-os",
  "tauri-plugin-process",
  "tauri-plugin-updater",
@@ -1483,6 +1656,12 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -2133,6 +2312,18 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
 
+[[package]]
+name = "mac-notification-sys"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3"
+dependencies = [
+ "cc",
+ "objc2",
+ "objc2-foundation",
+ "time",
+]
+
 [[package]]
 name = "markup5ever"
 version = "0.14.1"
@@ -2344,6 +2535,20 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "notify-rust"
+version = "4.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d782ebad628d1a24303e9a0aac4656115ceb482ba56c66c4af3d85530852d9a"
+dependencies = [
+ "futures-lite",
+ "log",
+ "mac-notification-sys",
+ "serde",
+ "tauri-winrt-notification",
+ "zbus",
+]
+
 [[package]]
 name = "nu-ansi-term"
 version = "0.50.3"
@@ -2670,6 +2875,16 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
 
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "os_info"
 version = "3.14.0"
@@ -2959,6 +3174,17 @@ version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
 
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
 [[package]]
 name = "pkg-config"
 version = "0.3.32"
@@ -2979,7 +3205,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
 dependencies = [
  "base64 0.22.1",
  "indexmap 2.13.0",
- "quick-xml",
+ "quick-xml 0.38.4",
  "serde",
  "time",
 ]
@@ -2997,6 +3223,20 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "potential_utf"
 version = "0.1.4"
@@ -3105,6 +3345,15 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "quick-xml"
 version = "0.38.4"
@@ -3160,6 +3409,16 @@ dependencies = [
  "rand_core 0.6.4",
 ]
 
+[[package]]
+name = "rand"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
 [[package]]
 name = "rand_chacha"
 version = "0.2.2"
@@ -3180,6 +3439,16 @@ dependencies = [
  "rand_core 0.6.4",
 ]
 
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
 [[package]]
 name = "rand_core"
 version = "0.5.1"
@@ -3198,6 +3467,15 @@ dependencies = [
  "getrandom 0.2.17",
 ]
 
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
 [[package]]
 name = "rand_hc"
 version = "0.2.0"
@@ -4374,6 +4652,25 @@ dependencies = [
  "url",
 ]
 
+[[package]]
+name = "tauri-plugin-notification"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc"
+dependencies = [
+ "log",
+ "notify-rust",
+ "rand 0.9.2",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.18",
+ "time",
+ "url",
+]
+
 [[package]]
 name = "tauri-plugin-os"
 version = "2.3.2"
@@ -4566,6 +4863,18 @@ dependencies = [
  "toml 0.9.12+spec-1.1.0",
 ]
 
+[[package]]
+name = "tauri-winrt-notification"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
+dependencies = [
+ "quick-xml 0.37.5",
+ "thiserror 2.0.18",
+ "windows",
+ "windows-version",
+]
+
 [[package]]
 name = "tempfile"
 version = "3.27.0"
@@ -4992,6 +5301,17 @@ version = "1.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
 
+[[package]]
+name = "uds_windows"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "unic-char-property"
 version = "0.9.0"
@@ -5926,6 +6246,9 @@ name = "winnow"
 version = "0.7.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
 
 [[package]]
 name = "winnow"
@@ -6138,6 +6461,67 @@ dependencies = [
  "synstructure",
 ]
 
+[[package]]
+name = "zbus"
+version = "5.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 0.7.15",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
+dependencies = [
+ "serde",
+ "winnow 0.7.15",
+ "zvariant",
+]
+
 [[package]]
 name = "zerocopy"
 version = "0.8.47"
@@ -6235,3 +6619,43 @@ name = "zmij"
 version = "1.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zvariant"
+version = "5.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 0.7.15",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.117",
+ "winnow 0.7.15",
+]
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index b621a43..41e5b74 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -19,10 +19,11 @@ vendored-openssl = ["git-sync-lib/vendored-openssl"]
 git-sync-lib    = { path = "../crates/git-sync-lib" }
 
 tauri                 = { version = "2", features = ["tray-icon"] }
-tauri-plugin-dialog   = "2"
-tauri-plugin-os       = "2"
-tauri-plugin-process  = "2"
-tauri-plugin-updater  = "2"
+tauri-plugin-dialog       = "2"
+tauri-plugin-notification = "2"
+tauri-plugin-os           = "2"
+tauri-plugin-process      = "2"
+tauri-plugin-updater      = "2"
 
 specta              = { version = "=2.0.0-rc.24", features = ["derive", "chrono"] }
 specta-typescript   = "0.0.11"
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 36f83e6..ebf8fad 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -10,6 +10,7 @@
 		"core:window:allow-toggle-maximize",
 		"core:window:allow-close",
 		"dialog:allow-open",
+		"notification:default",
 		"os:default",
 		"process:default",
 		"updater:default"
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 79c7c08..b081955 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -16,7 +16,7 @@ use tracing_subscriber::prelude::*;
 
 use config::{load_config, DesktopConfig};
 use log_layer::{FrontendLogEntry, TauriLogLayer};
-use status::AppStatus;
+use status::{AppStatus, SyncErrorPayload};
 use worker::BgCmd;
 
 // ── Bindings export (test-only utility) ───────────────────────────────────────
@@ -130,6 +130,7 @@ pub fn run() {
     let invoke_handler = builder.invoke_handler();
 
     tauri::Builder::default()
+        .plugin(tauri_plugin_notification::init())
         .plugin(tauri_plugin_updater::Builder::new().build())
         .plugin(tauri_plugin_process::init())
         .plugin(tauri_plugin_os::init())
@@ -163,15 +164,41 @@ pub fn run() {
 
             win_builder.build()?;
 
-            // Push status updates to the frontend whenever the worker sends a new snapshot.
+            // Push status updates to the frontend and send desktop notifications
+            // whenever a repository transitions into an error or conflict state.
             let app_handle = app.handle().clone();
             let mut rx = status_rx_forwarder;
             tauri::async_runtime::spawn(async move {
+                let mut prev_error_keys: Vec<Option<String>> = Vec::new();
                 loop {
                     if rx.changed().await.is_err() {
                         break;
                     }
                     let snapshot = rx.borrow_and_update().clone();
+
+                    // Reset tracking when the repo count changes (e.g. after reconfigure).
+                    if prev_error_keys.len() != snapshot.repos.len() {
+                        prev_error_keys = vec![None; snapshot.repos.len()];
+                    }
+
+                    for (i, repo) in snapshot.repos.iter().enumerate() {
+                        let key = error_key(&repo.error);
+                        if key.is_some() && key.as_deref() != prev_error_keys[i].as_deref() {
+                            if let Some(ref err) = repo.error {
+                                let (title, body) = notification_text(&repo.repo_path, err);
+                                use tauri_plugin_notification::NotificationExt;
+                                app_handle
+                                    .notification()
+                                    .builder()
+                                    .title(&title)
+                                    .body(&body)
+                                    .show()
+                                    .ok();
+                            }
+                        }
+                        prev_error_keys[i] = key;
+                    }
+
                     StatusUpdateEvent(snapshot).emit(&app_handle).ok();
                 }
             });
@@ -246,3 +273,57 @@ pub fn run() {
         .run(tauri::generate_context!())
         .expect("error while running tauri application");
 }
+
+/// Returns a short string key identifying the current error category, used to
+/// detect transitions into a new error state so we don't re-notify on every tick.
+fn error_key(error: &Option<SyncErrorPayload>) -> Option<String> {
+    error.as_ref().map(|e| match e {
+        SyncErrorPayload::Auth { .. } => "auth".to_string(),
+        SyncErrorPayload::Network { .. } => "network".to_string(),
+        SyncErrorPayload::Conflict { .. } => "conflict".to_string(),
+        SyncErrorPayload::ConflictBranch { branch, .. } => format!("conflict_branch:{}", branch),
+        SyncErrorPayload::Config { .. } => "config".to_string(),
+        SyncErrorPayload::State { .. } => "state".to_string(),
+        SyncErrorPayload::Unknown { .. } => "unknown".to_string(),
+    })
+}
+
+/// Builds the notification title and body for a repository error.
+fn notification_text(repo_path: &str, error: &SyncErrorPayload) -> (String, String) {
+    let repo_name = std::path::Path::new(repo_path)
+        .file_name()
+        .and_then(|n| n.to_str())
+        .unwrap_or(repo_path)
+        .to_string();
+
+    match error {
+        SyncErrorPayload::Conflict { message } => (
+            format!("{}: Merge conflict", repo_name),
+            message.clone(),
+        ),
+        SyncErrorPayload::ConflictBranch { branch, message } => (
+            format!("{}: Conflicts saved to branch", repo_name),
+            format!("{} — {}", branch, message),
+        ),
+        SyncErrorPayload::Auth { message } => (
+            format!("{}: Authentication failed", repo_name),
+            message.clone(),
+        ),
+        SyncErrorPayload::Network { message } => (
+            format!("{}: Network error", repo_name),
+            message.clone(),
+        ),
+        SyncErrorPayload::Config { message } => (
+            format!("{}: Configuration error", repo_name),
+            message.clone(),
+        ),
+        SyncErrorPayload::State { message } => (
+            format!("{}: Repository needs attention", repo_name),
+            message.clone(),
+        ),
+        SyncErrorPayload::Unknown { message } => (
+            format!("{}: Sync error", repo_name),
+            message.clone(),
+        ),
+    }
+}

From 01498b5eb1a312e65a0bb68aa58e1606cc45c950 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Apr 2026 03:00:16 +0000
Subject: [PATCH 2/2] fix: address code review feedback on notification logic

Agent-Logs-Url: https://github.com/cognitoforms/git-sync/sessions/80a246e7-607c-474b-a875-13ccb01838ac

Co-authored-by: michaelhthomas <18223295+michaelhthomas@users.noreply.github.com>
---
 src-tauri/src/lib.rs | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index b081955..3bbeaab 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -169,7 +169,8 @@ pub fn run() {
             let app_handle = app.handle().clone();
             let mut rx = status_rx_forwarder;
             tauri::async_runtime::spawn(async move {
-                let mut prev_error_keys: Vec<Option<String>> = Vec::new();
+                let mut prev_error_keys: Vec<Option<String>> =
+                    vec![None; rx.borrow().repos.len()];
                 loop {
                     if rx.changed().await.is_err() {
                         break;
@@ -281,7 +282,7 @@ fn error_key(error: &Option<SyncErrorPayload>) -> Option<String> {
         SyncErrorPayload::Auth { .. } => "auth".to_string(),
         SyncErrorPayload::Network { .. } => "network".to_string(),
         SyncErrorPayload::Conflict { .. } => "conflict".to_string(),
-        SyncErrorPayload::ConflictBranch { branch, .. } => format!("conflict_branch:{}", branch),
+        SyncErrorPayload::ConflictBranch { .. } => "conflict_branch".to_string(),
         SyncErrorPayload::Config { .. } => "config".to_string(),
         SyncErrorPayload::State { .. } => "state".to_string(),
         SyncErrorPayload::Unknown { .. } => "unknown".to_string(),
@@ -293,7 +294,7 @@ fn notification_text(repo_path: &str, error: &SyncErrorPayload) -> (String, Stri
     let repo_name = std::path::Path::new(repo_path)
         .file_name()
         .and_then(|n| n.to_str())
-        .unwrap_or(repo_path)
+        .unwrap_or("Unknown repository")
         .to_string();
 
     match error {
