Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add embeds to the `Textarea`
Sebastian Martinez committed 1 year ago
commit ca4d9d13fde0503dabd591d0f6167d9a4da93bb5
parent 9fd2d489ae9a02904001b9bddcfe4e66f2b04d48
20 files changed +1156 -278
modified Cargo.lock
@@ -241,7 +241,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -257,6 +257,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"

[[package]]
+
name = "ashpd"
+
version = "0.9.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4d43c03d9e36dd40cab48435be0b09646da362c278223ca535493877b2c1dee9"
+
dependencies = [
+
 "enumflags2",
+
 "futures-channel",
+
 "futures-util",
+
 "rand 0.8.5",
+
 "raw-window-handle",
+
 "serde",
+
 "serde_repr",
+
 "tokio",
+
 "url",
+
 "wayland-backend",
+
 "wayland-client",
+
 "wayland-protocols",
+
 "zbus",
+
]
+

+
[[package]]
name = "ast_node"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -265,10 +286,118 @@ dependencies = [
 "proc-macro2",
 "quote",
 "swc_macros_common",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
+
]
+

+
[[package]]
+
name = "async-broadcast"
+
version = "0.7.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e"
+
dependencies = [
+
 "event-listener",
+
 "event-listener-strategy",
+
 "futures-core",
+
 "pin-project-lite",
+
]
+

+
[[package]]
+
name = "async-channel"
+
version = "2.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
+
dependencies = [
+
 "concurrent-queue",
+
 "event-listener-strategy",
+
 "futures-core",
+
 "pin-project-lite",
+
]
+

+
[[package]]
+
name = "async-io"
+
version = "2.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
+
dependencies = [
+
 "async-lock",
+
 "cfg-if",
+
 "concurrent-queue",
+
 "futures-io",
+
 "futures-lite",
+
 "parking",
+
 "polling",
+
 "rustix",
+
 "slab",
+
 "tracing",
+
 "windows-sys 0.59.0",
+
]
+

+
[[package]]
+
name = "async-lock"
+
version = "3.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
+
dependencies = [
+
 "event-listener",
+
 "event-listener-strategy",
+
 "pin-project-lite",
+
]
+

+
[[package]]
+
name = "async-process"
+
version = "2.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
+
dependencies = [
+
 "async-channel",
+
 "async-io",
+
 "async-lock",
+
 "async-signal",
+
 "async-task",
+
 "blocking",
+
 "cfg-if",
+
 "event-listener",
+
 "futures-lite",
+
 "rustix",
+
 "tracing",
+
]
+

+
[[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.90",
]

[[package]]
+
name = "async-signal"
+
version = "0.2.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
+
dependencies = [
+
 "async-io",
+
 "async-lock",
+
 "atomic-waker",
+
 "cfg-if",
+
 "futures-core",
+
 "futures-io",
+
 "rustix",
+
 "signal-hook-registry",
+
 "slab",
+
 "windows-sys 0.59.0",
+
]
+

+
[[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.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -276,7 +405,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -303,6 +432,12 @@ dependencies = [
]

[[package]]
+
name = "atomic-waker"
+
version = "1.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+

+
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -528,6 +663,19 @@ dependencies = [
]

[[package]]
+
name = "blocking"
+
version = "1.6.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
+
dependencies = [
+
 "async-channel",
+
 "async-task",
+
 "futures-io",
+
 "futures-lite",
+
 "piper",
+
]
+

+
[[package]]
name = "blowfish"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -557,7 +705,7 @@ dependencies = [
 "proc-macro-crate 2.0.2",
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
 "syn_derive",
]

@@ -668,7 +816,7 @@ dependencies = [
 "glib",
 "libc",
 "once_cell",
-
 "thiserror",
+
 "thiserror 1.0.65",
]

[[package]]
@@ -711,7 +859,7 @@ dependencies = [
 "semver",
 "serde",
 "serde_json",
-
 "thiserror",
+
 "thiserror 1.0.65",
]

[[package]]
@@ -879,6 +1027,15 @@ dependencies = [
]

[[package]]
+
name = "concurrent-queue"
+
version = "2.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+
dependencies = [
+
 "crossbeam-utils",
+
]
+

+
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1069,7 +1226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -1085,7 +1242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
dependencies = [
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -1151,7 +1308,7 @@ dependencies = [
 "proc-macro2",
 "quote",
 "strsim",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -1162,7 +1319,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
 "darling_core",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -1215,7 +1372,7 @@ dependencies = [
 "swc_ecma_parser",
 "swc_eq_ignore_macros",
 "text_lines",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "unicode-width",
 "url",
]
@@ -1262,6 +1419,17 @@ dependencies = [
]

[[package]]
+
name = "derivative"
+
version = "2.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 1.0.109",
+
]
+

+
[[package]]
name = "derive_more"
version = "0.99.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1271,7 +1439,7 @@ dependencies = [
 "proc-macro2",
 "quote",
 "rustc_version",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -1314,6 +1482,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"

[[package]]
+
name = "dlib"
+
version = "0.5.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
+
dependencies = [
+
 "libloading",
+
]
+

+
[[package]]
name = "dlopen2"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1333,10 +1510,16 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
+
name = "downcast-rs"
+
version = "1.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+

+
[[package]]
name = "dpi"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1518,6 +1701,33 @@ dependencies = [
]

[[package]]
+
name = "endi"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
+

+
[[package]]
+
name = "enumflags2"
+
version = "0.7.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d"
+
dependencies = [
+
 "enumflags2_derive",
+
 "serde",
+
]
+

+
[[package]]
+
name = "enumflags2_derive"
+
version = "0.7.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.90",
+
]
+

+
[[package]]
name = "env_filter"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1560,6 +1770,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f"

[[package]]
+
name = "event-listener"
+
version = "5.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
+
dependencies = [
+
 "concurrent-queue",
+
 "parking",
+
 "pin-project-lite",
+
]
+

+
[[package]]
+
name = "event-listener-strategy"
+
version = "0.5.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2"
+
dependencies = [
+
 "event-listener",
+
 "pin-project-lite",
+
]
+

+
[[package]]
name = "exr"
version = "1.72.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1683,7 +1914,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -1709,7 +1940,7 @@ checksum = "32016f1242eb82af5474752d00fd8ebcd9004bd69b462b1c91de833972d08ed4"
dependencies = [
 "proc-macro2",
 "swc_macros_common",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -1761,6 +1992,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"

[[package]]
+
name = "futures-lite"
+
version = "2.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
+
dependencies = [
+
 "fastrand",
+
 "futures-core",
+
 "futures-io",
+
 "parking",
+
 "pin-project-lite",
+
]
+

+
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1768,7 +2012,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -1993,7 +2237,7 @@ dependencies = [
 "once_cell",
 "pin-project-lite",
 "smallvec",
-
 "thiserror",
+
 "thiserror 1.0.65",
]

[[package]]
@@ -2026,7 +2270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaeb9672a55e9e32cb6d3ef781e7526b25ab97d499fae71615649340b143424"
dependencies = [
 "serde",
-
 "thiserror",
+
 "thiserror 1.0.65",
]

[[package]]
@@ -2038,7 +2282,7 @@ dependencies = [
 "git-ref-format-core",
 "proc-macro-error",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -2074,7 +2318,7 @@ dependencies = [
 "memchr",
 "once_cell",
 "smallvec",
-
 "thiserror",
+
 "thiserror 1.0.65",
]

[[package]]
@@ -2088,7 +2332,7 @@ dependencies = [
 "proc-macro-error",
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -2178,7 +2422,7 @@ dependencies = [
 "proc-macro-error",
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -2235,6 +2479,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"

[[package]]
+
name = "hermit-abi"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
+

+
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2508,7 +2758,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -2535,7 +2785,7 @@ dependencies = [
 "Inflector",
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -2603,7 +2853,7 @@ dependencies = [
 "combine",
 "jni-sys",
 "log",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "walkdir",
 "windows-sys 0.45.0",
]
@@ -2647,7 +2897,7 @@ dependencies = [
 "jsonptr",
 "serde",
 "serde_json",
-
 "thiserror",
+
 "thiserror 1.0.65",
]

[[package]]
@@ -2952,7 +3202,7 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
-
 "hermit-abi",
+
 "hermit-abi 0.3.9",
 "libc",
 "wasi 0.11.0+wasi-snapshot-preview1",
 "windows-sys 0.52.0",
@@ -2974,7 +3224,7 @@ dependencies = [
 "once_cell",
 "png",
 "serde",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "windows-sys 0.59.0",
]

@@ -3001,7 +3251,7 @@ dependencies = [
 "ndk-sys",
 "num_enum",
 "raw-window-handle",
-
 "thiserror",
+
 "thiserror 1.0.65",
]

[[package]]
@@ -3026,6 +3276,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"

[[package]]
+
name = "nix"
+
version = "0.27.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
+
dependencies = [
+
 "bitflags 2.6.0",
+
 "cfg-if",
+
 "libc",
+
 "memoffset",
+
]
+

+
[[package]]
name = "nodrop"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3098,7 +3360,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -3160,7 +3422,7 @@ dependencies = [
 "proc-macro-crate 2.0.2",
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -3290,6 +3552,7 @@ checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
 "bitflags 2.6.0",
 "block2",
+
 "dispatch",
 "libc",
 "objc2",
]
@@ -3438,6 +3701,16 @@ 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_pipe"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3511,6 +3784,12 @@ dependencies = [
]

[[package]]
+
name = "parking"
+
version = "2.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+

+
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3673,7 +3952,7 @@ dependencies = [
 "phf_shared 0.11.2",
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -3716,6 +3995,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"

[[package]]
+
name = "piper"
+
version = "0.2.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
+
dependencies = [
+
 "atomic-waker",
+
 "fastrand",
+
 "futures-io",
+
]
+

+
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3750,7 +4040,7 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
dependencies = [
 "base64 0.22.1",
 "indexmap 2.6.0",
-
 "quick-xml",
+
 "quick-xml 0.32.0",
 "serde",
 "time",
]
@@ -3769,6 +4059,21 @@ dependencies = [
]

[[package]]
+
name = "polling"
+
version = "3.7.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
+
dependencies = [
+
 "cfg-if",
+
 "concurrent-queue",
+
 "hermit-abi 0.4.0",
+
 "pin-project-lite",
+
 "rustix",
+
 "tracing",
+
 "windows-sys 0.59.0",
+
]
+

+
[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3873,9 +4178,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"

[[package]]
name = "proc-macro2"
-
version = "1.0.88"
+
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
+
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
 "unicode-ident",
]
@@ -3896,7 +4201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
dependencies = [
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -3962,6 +4267,15 @@ dependencies = [
]

[[package]]
+
name = "quick-xml"
+
version = "0.36.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
+
dependencies = [
+
 "memchr",
+
]
+

+
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3998,7 +4312,7 @@ dependencies = [
 "siphasher 1.0.1",
 "sqlite",
 "tempfile",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "unicode-normalization",
]

@@ -4018,7 +4332,7 @@ dependencies = [
 "radicle-git-ext",
 "serde",
 "serde_json",
-
 "thiserror",
+
 "thiserror 1.0.65",
]

[[package]]
@@ -4038,7 +4352,7 @@ dependencies = [
 "serde",
 "sqlite",
 "ssh-key",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "zeroize",
]

@@ -4062,7 +4376,7 @@ dependencies = [
 "percent-encoding",
 "radicle-std-ext",
 "serde",
-
 "thiserror",
+
 "thiserror 1.0.65",
]

[[package]]
@@ -4073,7 +4387,7 @@ checksum = "fbee758010fb64482be4b18591fbeb3cbc15b16450d143edf4edb5484c7366c6"
dependencies = [
 "byteorder",
 "log",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "zeroize",
]

@@ -4099,7 +4413,7 @@ dependencies = [
 "radicle-std-ext",
 "serde",
 "tar",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "url",
]

@@ -4118,10 +4432,11 @@ dependencies = [
 "tauri",
 "tauri-build",
 "tauri-plugin-clipboard-manager",
+
 "tauri-plugin-dialog",
 "tauri-plugin-log",
 "tauri-plugin-shell",
 "tauri-plugin-window-state",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "tokio",
 "ts-rs",
]
@@ -4139,7 +4454,7 @@ dependencies = [
 "serde",
 "serde_json",
 "tempfile",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "tree-sitter-bash",
 "tree-sitter-c",
 "tree-sitter-css",
@@ -4275,7 +4590,7 @@ dependencies = [
 "rand_chacha 0.3.1",
 "simd_helpers",
 "system-deps",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "v_frame",
 "wasm-bindgen",
]
@@ -4337,7 +4652,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
 "getrandom 0.2.15",
 "libredox",
-
 "thiserror",
+
 "thiserror 1.0.65",
]

[[package]]
@@ -4426,6 +4741,29 @@ dependencies = [
]

[[package]]
+
name = "rfd"
+
version = "0.15.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8af382a047821a08aa6bfc09ab0d80ff48d45d8726f7cd8e44891f7cb4a4278e"
+
dependencies = [
+
 "ashpd",
+
 "block2",
+
 "glib-sys",
+
 "gobject-sys",
+
 "gtk-sys",
+
 "js-sys",
+
 "log",
+
 "objc2",
+
 "objc2-app-kit",
+
 "objc2-foundation",
+
 "raw-window-handle",
+
 "wasm-bindgen",
+
 "wasm-bindgen-futures",
+
 "web-sys",
+
 "windows-sys 0.48.0",
+
]
+

+
[[package]]
name = "rgb"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4576,7 +4914,7 @@ dependencies = [
 "proc-macro2",
 "quote",
 "serde_derive_internals",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -4668,7 +5006,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -4679,7 +5017,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -4713,7 +5051,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -4764,7 +5102,7 @@ dependencies = [
 "darling",
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -4800,6 +5138,17 @@ dependencies = [
]

[[package]]
+
name = "sha1"
+
version = "0.10.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+
dependencies = [
+
 "cfg-if",
+
 "cpufeatures",
+
 "digest",
+
]
+

+
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4837,6 +5186,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"

[[package]]
+
name = "signal-hook-registry"
+
version = "1.4.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
name = "signature"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5143,7 +5501,7 @@ dependencies = [
 "proc-macro2",
 "quote",
 "swc_macros_common",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -5243,7 +5601,7 @@ checksum = "695a1d8b461033d32429b5befbf0ad4d7a2c4d6ba9cd5ba4e0645c615839e8e4"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -5254,7 +5612,7 @@ checksum = "f486687bfb7b5c560868f69ed2d458b880cebc9babebcb67e49f31b55c5bf847"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -5277,7 +5635,7 @@ dependencies = [
 "proc-macro2",
 "quote",
 "swc_macros_common",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -5304,9 +5662,9 @@ dependencies = [

[[package]]
name = "syn"
-
version = "2.0.81"
+
version = "2.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "198514704ca887dd5a1e408c6c6cdcba43672f9b4062e1b24aa34e74e6d7faae"
+
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
dependencies = [
 "proc-macro2",
 "quote",
@@ -5322,7 +5680,7 @@ dependencies = [
 "proc-macro-error",
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -5400,7 +5758,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -5465,7 +5823,7 @@ dependencies = [
 "tauri-runtime",
 "tauri-runtime-wry",
 "tauri-utils",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "tokio",
 "tray-icon",
 "url",
@@ -5518,9 +5876,9 @@ dependencies = [
 "serde",
 "serde_json",
 "sha2",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
 "tauri-utils",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "time",
 "url",
 "uuid",
@@ -5536,7 +5894,7 @@ dependencies = [
 "heck 0.5.0",
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
 "tauri-codegen",
 "tauri-utils",
]
@@ -5571,7 +5929,48 @@ dependencies = [
 "serde_json",
 "tauri",
 "tauri-plugin",
-
 "thiserror",
+
 "thiserror 1.0.65",
+
]
+

+
[[package]]
+
name = "tauri-plugin-dialog"
+
version = "2.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8b59fd750551b1066744ab956a1cd6b1ea3e1b3763b0b9153ac27a044d596426"
+
dependencies = [
+
 "log",
+
 "raw-window-handle",
+
 "rfd",
+
 "serde",
+
 "serde_json",
+
 "tauri",
+
 "tauri-plugin",
+
 "tauri-plugin-fs",
+
 "thiserror 2.0.6",
+
 "url",
+
]
+

+
[[package]]
+
name = "tauri-plugin-fs"
+
version = "2.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a1a1edf18000f02903a7c2e5997fb89aca455ecbc0acc15c6535afbb883be223"
+
dependencies = [
+
 "anyhow",
+
 "dunce",
+
 "glob",
+
 "percent-encoding",
+
 "schemars",
+
 "serde",
+
 "serde_json",
+
 "serde_repr",
+
 "tauri",
+
 "tauri-plugin",
+
 "tauri-utils",
+
 "thiserror 2.0.6",
+
 "toml 0.8.2",
+
 "url",
+
 "uuid",
]

[[package]]
@@ -5592,7 +5991,7 @@ dependencies = [
 "swift-rs",
 "tauri",
 "tauri-plugin",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "time",
]

@@ -5613,7 +6012,7 @@ dependencies = [
 "shared_child",
 "tauri",
 "tauri-plugin",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "tokio",
]

@@ -5629,7 +6028,7 @@ dependencies = [
 "serde_json",
 "tauri",
 "tauri-plugin",
-
 "thiserror",
+
 "thiserror 1.0.65",
]

[[package]]
@@ -5646,7 +6045,7 @@ dependencies = [
 "serde",
 "serde_json",
 "tauri-utils",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "url",
 "windows",
]
@@ -5708,7 +6107,7 @@ dependencies = [
 "serde_with",
 "serialize-to-javascript",
 "swift-rs",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "toml 0.8.2",
 "url",
 "urlpattern",
@@ -5772,7 +6171,7 @@ dependencies = [
 "radicle-types",
 "serde",
 "serde_json",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "tokio",
 "tower-http",
]
@@ -5798,7 +6197,16 @@ version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
dependencies = [
-
 "thiserror-impl",
+
 "thiserror-impl 1.0.65",
+
]
+

+
[[package]]
+
name = "thiserror"
+
version = "2.0.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47"
+
dependencies = [
+
 "thiserror-impl 2.0.6",
]

[[package]]
@@ -5809,7 +6217,18 @@ checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
+
]
+

+
[[package]]
+
name = "thiserror-impl"
+
version = "2.0.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.90",
]

[[package]]
@@ -5882,8 +6301,10 @@ dependencies = [
 "libc",
 "mio",
 "pin-project-lite",
+
 "signal-hook-registry",
 "socket2",
 "tokio-macros",
+
 "tracing",
 "windows-sys 0.52.0",
]

@@ -5895,7 +6316,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -6032,7 +6453,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -6061,7 +6482,7 @@ dependencies = [
 "once_cell",
 "png",
 "serde",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "windows-sys 0.59.0",
]

@@ -6127,7 +6548,7 @@ dependencies = [
 "lazy_static",
 "regex",
 "streaming-iterator",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "tree-sitter",
]

@@ -6262,7 +6683,7 @@ dependencies = [
 "dprint-plugin-typescript",
 "lazy_static",
 "serde_json",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "ts-rs-macros",
]

@@ -6274,7 +6695,7 @@ checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
 "termcolor",
]

@@ -6297,6 +6718,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"

[[package]]
+
name = "uds_windows"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
+
dependencies = [
+
 "memoffset",
+
 "tempfile",
+
 "winapi",
+
]
+

+
[[package]]
name = "unic-char-property"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6540,7 +6972,7 @@ dependencies = [
 "once_cell",
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
 "wasm-bindgen-shared",
]

@@ -6574,7 +7006,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
 "wasm-bindgen-backend",
 "wasm-bindgen-shared",
]
@@ -6599,6 +7031,66 @@ dependencies = [
]

[[package]]
+
name = "wayland-backend"
+
version = "0.3.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6"
+
dependencies = [
+
 "cc",
+
 "downcast-rs",
+
 "rustix",
+
 "scoped-tls",
+
 "smallvec",
+
 "wayland-sys",
+
]
+

+
[[package]]
+
name = "wayland-client"
+
version = "0.31.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280"
+
dependencies = [
+
 "bitflags 2.6.0",
+
 "rustix",
+
 "wayland-backend",
+
 "wayland-scanner",
+
]
+

+
[[package]]
+
name = "wayland-protocols"
+
version = "0.32.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e"
+
dependencies = [
+
 "bitflags 2.6.0",
+
 "wayland-backend",
+
 "wayland-client",
+
 "wayland-scanner",
+
]
+

+
[[package]]
+
name = "wayland-scanner"
+
version = "0.31.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3"
+
dependencies = [
+
 "proc-macro2",
+
 "quick-xml 0.36.2",
+
 "quote",
+
]
+

+
[[package]]
+
name = "wayland-sys"
+
version = "0.31.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09"
+
dependencies = [
+
 "dlib",
+
 "log",
+
 "pkg-config",
+
]
+

+
[[package]]
name = "web-sys"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6674,7 +7166,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -6683,7 +7175,7 @@ version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886"
dependencies = [
-
 "thiserror",
+
 "thiserror 1.0.65",
 "windows",
 "windows-core 0.58.0",
]
@@ -6779,7 +7271,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -6790,7 +7282,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -7096,7 +7588,7 @@ dependencies = [
 "sha2",
 "soup3",
 "tao-macros",
-
 "thiserror",
+
 "thiserror 1.0.65",
 "webkit2gtk",
 "webkit2gtk-sys",
 "webview2-com",
@@ -7165,6 +7657,75 @@ dependencies = [
]

[[package]]
+
name = "xdg-home"
+
version = "1.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
+
dependencies = [
+
 "libc",
+
 "windows-sys 0.59.0",
+
]
+

+
[[package]]
+
name = "zbus"
+
version = "4.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7b8e3d6ae3342792a6cc2340e4394334c7402f3d793b390d2c5494a4032b3030"
+
dependencies = [
+
 "async-broadcast",
+
 "async-process",
+
 "async-recursion",
+
 "async-trait",
+
 "derivative",
+
 "enumflags2",
+
 "event-listener",
+
 "futures-core",
+
 "futures-sink",
+
 "futures-util",
+
 "hex",
+
 "nix",
+
 "ordered-stream",
+
 "rand 0.8.5",
+
 "serde",
+
 "serde_repr",
+
 "sha1",
+
 "static_assertions",
+
 "tokio",
+
 "tracing",
+
 "uds_windows",
+
 "windows-sys 0.52.0",
+
 "xdg-home",
+
 "zbus_macros",
+
 "zbus_names",
+
 "zvariant",
+
]
+

+
[[package]]
+
name = "zbus_macros"
+
version = "4.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b7a3e850ff1e7217a3b7a07eba90d37fe9bb9e89a310f718afcde5885ca9b6d7"
+
dependencies = [
+
 "proc-macro-crate 1.3.1",
+
 "proc-macro2",
+
 "quote",
+
 "regex",
+
 "syn 1.0.109",
+
 "zvariant_utils",
+
]
+

+
[[package]]
+
name = "zbus_names"
+
version = "3.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
+
dependencies = [
+
 "serde",
+
 "static_assertions",
+
 "zvariant",
+
]
+

+
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7182,7 +7743,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.81",
+
 "syn 2.0.90",
]

[[package]]
@@ -7214,3 +7775,41 @@ checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768"
dependencies = [
 "zune-core",
]
+

+
[[package]]
+
name = "zvariant"
+
version = "4.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4e09e8be97d44eeab994d752f341e67b3b0d80512a8b315a0671d47232ef1b65"
+
dependencies = [
+
 "endi",
+
 "enumflags2",
+
 "serde",
+
 "static_assertions",
+
 "url",
+
 "zvariant_derive",
+
]
+

+
[[package]]
+
name = "zvariant_derive"
+
version = "4.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "72a5857e2856435331636a9fbb415b09243df4521a267c5bedcd5289b4d5799e"
+
dependencies = [
+
 "proc-macro-crate 1.3.1",
+
 "proc-macro2",
+
 "quote",
+
 "syn 1.0.109",
+
 "zvariant_utils",
+
]
+

+
[[package]]
+
name = "zvariant_utils"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 1.0.109",
+
]
modified crates/radicle-tauri/Cargo.toml
@@ -31,6 +31,7 @@ tauri-plugin-window-state = { version = "2.0.1" }
thiserror = { version = "1.0.64" }
ts-rs = { version = "10.0.0", features = ["serde-json-impl", "no-serde-warnings"] }
tokio = { version = "1.40.0", features = ["time"] }
+
tauri-plugin-dialog = { version = "2.2.0" }

[features]
# by default Tauri runs in production mode
modified crates/radicle-tauri/capabilities/default.json
@@ -16,6 +16,7 @@
    "shell:allow-open",
    "clipboard-manager:default",
    "clipboard-manager:allow-write-text",
-
    "log:default"
+
    "log:default",
+
    "dialog:default"
  ]
}
modified crates/radicle-tauri/src/commands/cob.rs
@@ -1,3 +1,7 @@
+
use std::path::PathBuf;
+

+
use anyhow::{Context, Result};
+

use radicle::cob;
use radicle::git;
use radicle::identity;
@@ -5,6 +9,8 @@ use radicle_types as types;
use radicle_types::error::Error;
use radicle_types::traits::cobs::Cobs;
use radicle_types::traits::thread::Thread;
+
use tauri_plugin_clipboard_manager::ClipboardExt;
+
use tauri_plugin_dialog::DialogExt;

use crate::AppState;

@@ -13,22 +19,69 @@ pub mod issue;
pub mod patch;

#[tauri::command]
-
pub async fn get_file_by_oid(
+
pub async fn get_embed(
    ctx: tauri::State<'_, AppState>,
    rid: identity::RepoId,
    oid: git::Oid,
-
) -> Result<String, Error> {
+
) -> Result<Vec<u8>, Error> {
    ctx.get_embed(rid, oid)
}

#[tauri::command]
-
pub async fn save_embed(
+
pub async fn save_embed_by_path(
+
    ctx: tauri::State<'_, AppState>,
+
    rid: identity::RepoId,
+
    path: PathBuf,
+
) -> Result<git::Oid, Error> {
+
    ctx.save_embed_by_path(rid, path)
+
}
+

+
#[tauri::command]
+
pub async fn save_embed_by_clipboard(
+
    app_handle: tauri::AppHandle,
    ctx: tauri::State<'_, AppState>,
    rid: identity::RepoId,
-
    name: &str,
-
    bytes: &[u8],
+
    name: String,
) -> Result<git::Oid, Error> {
-
    ctx.save_embed(rid, name, bytes)
+
    let content = app_handle
+
        .clipboard()
+
        .read_image()
+
        .map(|i| i.rgba().to_vec())
+
        .context("Not able to read the image from the clipboard")?;
+

+
    ctx.save_embed_by_bytes(rid, name, content)
+
}
+

+
#[tauri::command]
+
pub async fn save_embed_by_bytes(
+
    ctx: tauri::State<'_, AppState>,
+
    rid: identity::RepoId,
+
    name: String,
+
    bytes: Vec<u8>,
+
) -> Result<git::Oid, Error> {
+
    ctx.save_embed_by_bytes(rid, name, bytes)
+
}
+

+
#[tauri::command]
+
pub async fn save_embed_to_disk(
+
    app_handle: tauri::AppHandle,
+
    ctx: tauri::State<'_, AppState>,
+
    rid: identity::RepoId,
+
    oid: git::Oid,
+
    name: String,
+
) -> Result<(), Error> {
+
    let path = app_handle
+
        .dialog()
+
        .file()
+
        .set_file_name(name)
+
        .blocking_save_file()
+
        .context("no path defined")?;
+

+
    let path = path
+
        .into_path()
+
        .context("Not able to convert into PathBuf")?;
+

+
    ctx.save_embed_to_disk(rid, oid, path)
}

#[tauri::command]
modified crates/radicle-tauri/src/lib.rs
@@ -13,7 +13,9 @@ use commands::{auth, cob, diff, profile, repo, thread};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    #[cfg(debug_assertions)]
-
    let builder = tauri::Builder::default().plugin(tauri_plugin_log::Builder::new().build());
+
    let builder = tauri::Builder::default()
+
        .plugin(tauri_plugin_dialog::init())
+
        .plugin(tauri_plugin_log::Builder::new().build());
    #[cfg(not(debug_assertions))]
    let builder = tauri::Builder::default();

@@ -74,9 +76,12 @@ pub fn run() {
            repo::diff_stats,
            repo::list_commits,
            diff::get_diff,
-
            cob::get_file_by_oid,
+
            cob::get_embed,
+
            cob::save_embed_to_disk,
            cob::activity_by_id,
-
            cob::save_embed,
+
            cob::save_embed_by_path,
+
            cob::save_embed_by_clipboard,
+
            cob::save_embed_by_bytes,
            cob::issue::list_issues,
            cob::issue::issue_by_id,
            cob::issue::comment_threads_by_issue_id,
modified crates/radicle-types/src/error.rs
@@ -9,6 +9,14 @@ pub enum Error {
    #[error(transparent)]
    Profile(#[from] radicle::profile::Error),

+
    /// Anyhow error.
+
    #[error(transparent)]
+
    Anyhow(#[from] anyhow::Error),
+

+
    /// Io error.
+
    #[error(transparent)]
+
    Io(#[from] std::io::Error),
+

    /// Crypto error.
    #[error(transparent)]
    Crypto(#[from] radicle::crypto::ssh::keystore::Error),
modified crates/radicle-types/src/traits/repo.rs
@@ -202,6 +202,7 @@ pub trait Repo: Profile {
#[allow(clippy::unwrap_used)]
mod test {
    use std::str::FromStr;
+
    use std::vec;

    use radicle::crypto::test::signer::MockSigner;
    use radicle::{git, test};
modified crates/radicle-types/src/traits/thread.rs
@@ -1,4 +1,5 @@
-
use base64::{engine::general_purpose::STANDARD, Engine as _};
+
use std::fs;
+

use localtime::LocalTime;

use radicle::cob;
@@ -14,23 +15,51 @@ use crate::error::Error;
use crate::traits::Profile;

pub trait Thread: Profile {
-
    fn get_embed(&self, rid: identity::RepoId, oid: git::Oid) -> Result<String, Error> {
+
    fn get_embed(&self, rid: identity::RepoId, oid: git::Oid) -> Result<Vec<u8>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let blob = repo.blob(oid)?;
+

+
        Ok::<_, Error>(blob.content().to_vec())
+
    }
+

+
    fn save_embed_to_disk(
+
        &self,
+
        rid: identity::RepoId,
+
        oid: git::Oid,
+
        path: std::path::PathBuf,
+
    ) -> Result<(), Error> {
        let profile = self.profile();
        let repo = profile.storage.repository(rid)?;
        let blob = repo.blob(oid)?;
+
        fs::write(path, blob.content())?;

-
        Ok::<_, Error>(STANDARD.encode(blob.content()))
+
        Ok::<_, Error>(())
+
    }
+

+
    fn save_embed_by_path(
+
        &self,
+
        rid: identity::RepoId,
+
        path: std::path::PathBuf,
+
    ) -> Result<git::Oid, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let bytes = fs::read(path.clone())?;
+
        let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("embed");
+
        let embed = radicle::cob::Embed::<git::Oid>::store(name, &bytes, &repo.backend)?;
+

+
        Ok(embed.oid())
    }

-
    fn save_embed(
+
    fn save_embed_by_bytes(
        &self,
        rid: identity::RepoId,
-
        name: &str,
-
        bytes: &[u8],
+
        name: String,
+
        bytes: Vec<u8>,
    ) -> Result<git::Oid, Error> {
        let profile = self.profile();
        let repo = profile.storage.repository(rid)?;
-
        let embed = radicle::cob::Embed::<git::Oid>::store(name, bytes, &repo.backend)?;
+
        let embed = radicle::cob::Embed::<git::Oid>::store(&name, &bytes, &repo.backend)?;

        Ok(embed.oid())
    }
modified crates/test-http-api/src/api.rs
@@ -1,4 +1,5 @@
use std::ops::Deref;
+
use std::path::PathBuf;
use std::sync::Arc;

use axum::extract::State;
@@ -67,8 +68,11 @@ pub fn router(ctx: Context) -> Router {
        .route("/list_patches", post(patches_handler))
        .route("/patch_by_id", post(patch_handler))
        .route("/revisions_by_patch", post(revision_handler))
-
        .route("/get_file_by_oid", post(get_embeds_handler))
-
        .route("/save_embed", post(save_embed_handler))
+
        .route("/get_embed", post(get_embeds_handler))
+
        .route("/save_embed_by_path", post(save_embed_handler))
+
        .route("/save_embed_by_clipboard", post(save_embed_handler))
+
        .route("/save_embed_by_bytes", post(save_embed_handler))
+
        .route("/save_embed_to_disk", post(save_embed_handler))
        .layer(
            CorsLayer::new()
                .allow_origin(cors::Any)
@@ -255,15 +259,14 @@ async fn get_embeds_handler(
#[derive(Serialize, Deserialize)]
struct CreateEmbedBody {
    pub rid: identity::RepoId,
-
    pub name: String,
-
    pub content: Vec<u8>,
+
    pub path: PathBuf,
}

async fn save_embed_handler(
    State(ctx): State<Context>,
-
    Json(CreateEmbedBody { rid, name, content }): Json<CreateEmbedBody>,
+
    Json(CreateEmbedBody { rid, path }): Json<CreateEmbedBody>,
) -> impl IntoResponse {
-
    let embed = ctx.save_embed(rid, &name, &content)?;
+
    let embed = ctx.save_embed_by_path(rid, path)?;

    Ok::<_, Error>(Json(embed))
}
modified package-lock.json
@@ -11,7 +11,8 @@
      "license": "MIT",
      "dependencies": {
        "@tauri-apps/api": "^2.1.1",
-
        "@tauri-apps/plugin-clipboard-manager": "^2.0.0",
+
        "@tauri-apps/plugin-clipboard-manager": "^2.2.0",
+
        "@tauri-apps/plugin-dialog": "^2.2.0",
        "@tauri-apps/plugin-log": "^2.0.0",
        "@tauri-apps/plugin-shell": "^2.0.0",
        "@tauri-apps/plugin-window-state": "^2.0.0"
@@ -1341,10 +1342,17 @@
      }
    },
    "node_modules/@tauri-apps/plugin-clipboard-manager": {
-
      "version": "2.0.0",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0.tgz",
-
      "integrity": "sha512-V1sXmbjnwfXt/r48RJMwfUmDMSaP/8/YbH4CLNxt+/sf1eHlIP8PRFdFDQwLN0cNQKu2rqQVbG/Wc/Ps6cDUhw==",
-
      "license": "MIT OR Apache-2.0",
+
      "version": "2.2.0",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.2.0.tgz",
+
      "integrity": "sha512-sIBrW/HioKq2vqomwwcU/Y8ygAv3DlS32yKPBX5XijCc0IyQKiDxYpGqmvE9DC5Y0lNJ/G53dfS961B31wjJ1g==",
+
      "dependencies": {
+
        "@tauri-apps/api": "^2.0.0"
+
      }
+
    },
+
    "node_modules/@tauri-apps/plugin-dialog": {
+
      "version": "2.2.0",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.2.0.tgz",
+
      "integrity": "sha512-6bLkYK68zyK31418AK5fNccCdVuRnNpbxquCl8IqgFByOgWFivbiIlvb79wpSXi0O+8k8RCSsIpOquebusRVSg==",
      "dependencies": {
        "@tauri-apps/api": "^2.0.0"
      }
modified package.json
@@ -25,7 +25,8 @@
  "license": "MIT",
  "dependencies": {
    "@tauri-apps/api": "^2.1.1",
-
    "@tauri-apps/plugin-clipboard-manager": "^2.0.0",
+
    "@tauri-apps/plugin-clipboard-manager": "^2.2.0",
+
    "@tauri-apps/plugin-dialog": "^2.2.0",
    "@tauri-apps/plugin-log": "^2.0.0",
    "@tauri-apps/plugin-shell": "^2.0.0",
    "@tauri-apps/plugin-window-state": "^2.0.0"
modified public/index.css
@@ -64,6 +64,12 @@ body {
  font-weight: var(--font-weight-regular);
}

+
@media (max-width: 1600px) {
+
  .global-hide-on-small-desktop-down {
+
    display: none !important;
+
  }
+
}
+

:root {
  --elevation-low: 0 0 48px 0 #000000ee;
}
modified src/components/Border.svelte
@@ -6,6 +6,7 @@
    variant: "primary" | "secondary" | "ghost" | "float" | "danger";
    hoverable?: boolean;
    onclick?: () => void;
+
    stylePosition?: string;
    stylePadding?: string;
    styleHeight?: string;
    styleMinHeight?: string;
@@ -27,6 +28,7 @@
    stylePadding,
    styleHeight,
    styleMinHeight,
+
    stylePosition,
    styleWidth,
    styleCursor = "default",
    styleGap = "0.5rem",
@@ -222,6 +224,7 @@
  <div class="p3-2"></div>
  <div
    class="p3-3"
+
    style:position={stylePosition}
    style:padding={stylePadding}
    style:gap={styleGap}
    style:overflow={styleOverflow}
modified src/components/ExtendedTextarea.svelte
@@ -1,9 +1,15 @@
<script lang="ts">
+
  import type { UnlistenFn } from "@tauri-apps/api/event";
  import type { ComponentProps } from "svelte";
-
  import type { Embed } from "@bindings/cob/thread/Embed";

  import * as utils from "@app/lib/utils";

+
  import type { Embed } from "@bindings/cob/thread/Embed";
+
  import { invoke } from "@app/lib/invoke";
+
  import { listen } from "@tauri-apps/api/event";
+
  import { onDestroy, onMount } from "svelte";
+
  import { open } from "@tauri-apps/plugin-dialog";
+

  import Button from "./Button.svelte";
  import Icon from "./Icon.svelte";
  import Markdown from "./Markdown.svelte";
@@ -11,6 +17,7 @@
  import OutlineButton from "./OutlineButton.svelte";

  interface Props {
+
    styleMinHeight?: string;
    rid: string;
    placeholder?: string;
    submitCaption?: string;
@@ -21,6 +28,7 @@
    submitInProgress?: boolean;
    disallowEmptyBody?: boolean;
    isValid?: () => boolean;
+
    preview?: boolean;
    stylePadding?: string;
    borderVariant?: ComponentProps<typeof Textarea>["borderVariant"];
    submit: (opts: {
@@ -32,6 +40,8 @@

  /* eslint-disable prefer-const */
  let {
+
    preview = $bindable(false),
+
    styleMinHeight,
    rid,
    placeholder = "Leave your comment",
    submitCaption = "Comment",
@@ -49,12 +59,124 @@
  }: Props = $props();
  /* eslint-enable prefer-const */

-
  let preview: boolean = $state(false);
-
  let selectionStart = $state(0);
-
  let selectionEnd = $state(0);
-
  let inputFiles: FileList | undefined = $state(undefined);
+
  let selectionStart = $state(body.length);
+
  let selectionEnd = $state(body.length);
+
  let draggingOver = $state(false);
+
  let dragEnterUnlistenFn: UnlistenFn | undefined = undefined;
+
  let dragLeaveUnlistenFn: UnlistenFn | undefined = undefined;
+
  let dragDropUnlistenFn: UnlistenFn | undefined = undefined;
+

+
  function updateBodyAndSelection(input: string[], pre: string, after: string) {
+
    const allEmbeds = input.join("");
+
    body = pre.concat(allEmbeds, after);
+
    selectionStart = pre.length + allEmbeds.length;
+
    selectionEnd = pre.length + allEmbeds.length;
+
  }
+

+
  function splitBody() {
+
    return [body.substring(0, selectionStart), body.substring(selectionStart)];
+
  }
+

+
  onMount(async () => {
+
    if (window.__TAURI_INTERNALS__) {
+
      dragEnterUnlistenFn = await listen("tauri://drag-enter", () => {
+
        draggingOver = true;
+
      });
+

+
      dragLeaveUnlistenFn = await listen("tauri://drag-leave", () => {
+
        draggingOver = false;
+
      });
+

+
      dragDropUnlistenFn = await listen<{
+
        paths: string[];
+
        position: { x: number; y: number };
+
      }>("tauri://drag-drop", async event => {
+
        draggingOver = false;
+
        const [preBody, afterBody] = splitBody();
+

+
        return Promise.all(
+
          event.payload.paths.map(async path => {
+
            const name = path.split("/").at(-1);
+
            const uploadLabel = `[Uploading ${name}...]()\n`;
+

+
            body = preBody.concat(uploadLabel, afterBody);
+
            const oid = await invoke<string>("save_embed_by_path", {
+
              rid,
+
              path,
+
            }).catch(console.error);
+
            return `[${name}](${oid})\n`;
+
          }),
+
        ).then(texts => updateBodyAndSelection(texts, preBody, afterBody));
+
      });
+
    }
+
  });
+

+
  onDestroy(() => {
+
    if (dragEnterUnlistenFn) dragEnterUnlistenFn();
+
    if (dragLeaveUnlistenFn) dragLeaveUnlistenFn();
+
    if (dragDropUnlistenFn) dragDropUnlistenFn();
+
  });
+

+
  async function attachEmbedsByPaths(paths: string[]) {
+
    const [preBody, afterBody] = splitBody();
+

+
    return Promise.all(
+
      paths.map(async path => {
+
        const name = path.split("/").at(-1);
+
        const uploadLabel = `[Uploading ${name}...]()\n`;
+
        body = preBody.concat(uploadLabel, afterBody);
+
        const oid = await invoke<string>("save_embed_by_path", {
+
          rid,
+
          path,
+
        }).catch(console.error);
+
        return `[${name}](${oid})\n`;
+
      }),
+
    ).then(texts => updateBodyAndSelection(texts, preBody, afterBody));
+
  }
+

+
  async function handlePaste(e: ClipboardEvent) {
+
    if (e.clipboardData?.files && e.clipboardData.files.length > 0) {
+
      e.preventDefault();
+
      const [preBody, afterBody] = splitBody();
+
      // We read the buffer on the backend, if it's a image buffer.
+
      if (e.clipboardData.items.length === 1) {
+
        const file = e.clipboardData.files[0];
+
        const uploadLabel = `[Uploading...]()\n`;
+
        body = preBody.concat(uploadLabel, afterBody);
+
        const oid = await invoke<string>("save_embed_by_clipboard", {
+
          name: file.name,
+
          rid,
+
        }).catch(console.error);
+
        body = preBody.concat(`[${file.name}](${oid})\n`, afterBody);
+
      } else {
+
        return Promise.all(
+
          Array.from(e.clipboardData.files).map(async file => {
+
            const arrayBuffer = await file.arrayBuffer();
+
            const bytes = new Uint8Array(arrayBuffer);
+
            const uploadLabel = `[Uploading ${file.name}...]()\n`;
+
            body = preBody.concat(uploadLabel, afterBody);
+
            const oid = await invoke<string>("save_embed_by_bytes", {
+
              rid,
+
              name: file.name,
+
              bytes,
+
            }).catch(console.error);
+
            return `[${file.name}](${oid})\n`;
+
          }),
+
        ).then(texts => updateBodyAndSelection(texts, preBody, afterBody));
+
      }
+
    } else {
+
      // In case that the clipboard data isn't an array of files,
+
      // we want to make use of the default behavior and insert the clipboard content.
+
    }
+
  }

-
  const inputId = `input-label-${crypto.randomUUID()}`;
+
  function selectFiles() {
+
    void open({ multiple: true }).then(paths => {
+
      if (paths) {
+
        void attachEmbedsByPaths(paths);
+
      }
+
    });
+
  }

  function submitFn() {
    void submit({ comment: body, embeds })
@@ -87,16 +209,19 @@
  .buttons {
    display: flex;
    margin-left: auto;
-
    gap: 1rem;
+
    gap: 0.5rem;
  }
+

  .caption {
    font-size: var(--font-size-small);
    color: var(--color-fill-gray);
    display: flex;
-
    align-items: center;
+
    flex-wrap: wrap;
+
    align-items: flex-start;
    gap: 0.25rem;
  }
  .preview {
+
    width: 100%;
    font-size: var(--font-size-small);
    min-height: 109px;
    padding: 0.75rem;
@@ -111,17 +236,14 @@
      <Markdown {rid} breaks content={body} />
    </div>
  {:else}
-
    <input
-
      multiple
-
      bind:files={inputFiles}
-
      style:display="none"
-
      type="file"
-
      id={inputId} />
    <Textarea
+
      {draggingOver}
      {borderVariant}
      {stylePadding}
+
      {styleMinHeight}
      bind:selectionEnd
      bind:selectionStart
+
      onpaste={handlePaste}
      {focus}
      submit={() => submit({ comment: body, embeds })}
      bind:value={body}
@@ -135,20 +257,37 @@
        preview = false;
        close();
      }}>
-
      <Icon name="cross" />Discard
+
      <Icon name="cross" />
+
      <span class="global-hide-on-small-desktop-down">Discard</span>
    </OutlineButton>
    {#if !preview}
      <div class="caption">
-
        <Icon name="markdown" />
-
        Markdown is supported. Press {utils.modifierKey()}↵ to submit.
+
        Drag and drop files to add them.
+
        <div style="display: flex; align-items: center; gap: 0.25rem;">
+
          <Icon name="markdown" />
+
          Markdown is supported.
+
        </div>
+
        <span class="global-hide-on-small-desktop-down">
+
          <br />
+
        </span>
+
        <span class="global-hide-on-small-desktop-down">
+
          Press {utils.modifierKey()}↵ to submit.
+
        </span>
      </div>
    {/if}
    <div class="buttons">
+
      <OutlineButton variant="ghost" onclick={selectFiles}>
+
        <Icon name="attachment" />
+
        <span class="global-hide-on-small-desktop-down">Attach</span>
+
      </OutlineButton>
      <OutlineButton
        variant="ghost"
        disabled={body.trim() === ""}
        onclick={() => (preview = !preview)}>
-
        <Icon name={preview ? "pen" : "eye"} />{preview ? "Edit" : "Preview"}
+
        <Icon name={preview ? "pen" : "eye"} />
+
        <span class="global-hide-on-small-desktop-down">
+
          {preview ? "Edit" : "Preview"}
+
        </span>
      </OutlineButton>
      <Button
        variant="ghost"
@@ -157,11 +296,13 @@
          (disallowEmptyBody && body.trim() === "")}
        onclick={submitFn}>
        <Icon name="checkmark" />
-
        {#if submitInProgress}
-
          Saving…
-
        {:else}
-
          {submitCaption}
-
        {/if}
+
        <span class="global-hide-on-small-desktop-down">
+
          {#if submitInProgress}
+
            Saving…
+
          {:else}
+
            {submitCaption}
+
          {/if}
+
        </span>
      </Button>
    </div>
  </div>
modified src/components/Icon.svelte
@@ -6,6 +6,7 @@
    onclick?: () => void;
    disabled?: boolean;
    name:
+
      | "attachment"
      | "arrow-left"
      | "arrow-right"
      | "checkmark"
@@ -108,6 +109,19 @@
    <path d="M10 3H9.00003V13H10V3Z" />
    <path d="M13 6H12V7H13V6Z" />
    <path d="M14 7H13V8H14V7Z" />
+
  {:else if name === "attachment"}
+
    <path d="M4 4H12V5H4V4Z" />
+
    <path d="M4 11H11V12H4V11Z" />
+
    <path d="M13 6H14V8H13V6Z" />
+
    <path d="M2 6H3V10H2V6Z" />
+
    <path d="M12 5L13 5V6H12V5Z" />
+
    <path d="M3 5L4 5V6L3 6L3 5Z" />
+
    <path d="M12 8H13V9H12V8Z" />
+
    <path d="M3 10H4L4 11H3L3 10Z" />
+
    <path d="M5 9L12 9V10H5V9Z" />
+
    <path d="M4 8H5V9L4 9V8Z" />
+
    <path d="M5 6H11V7H5V6Z" />
+
    <path d="M4 7H5L5 8H4L4 7Z" />
  {:else if name === "checkmark"}
    <path d="M7 11V12H6V11H7Z" />
    <path d="M8 10V11L7 11L7 10H8Z" />
modified src/components/Markdown.svelte
@@ -41,6 +41,9 @@
  }

  $effect(() => {
+
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+
    content;
+

    void tick().then(() => {
      for (const e of container.querySelectorAll("a")) {
        try {
@@ -59,6 +62,66 @@
        ) {
          e.classList.add("no-underline");
        }
+

+
        // Iterate over all links, and try to add a base64 preview beneath it.
+
        const href = e.getAttribute("href");
+

+
        // If the markdown link is an oid embed
+
        if (href && isCommit(href)) {
+
          e.style.display = "block";
+
          e.onclick = event => {
+
            event.preventDefault();
+
            invoke("save_embed_to_disk", {
+
              rid,
+
              oid: href,
+
              name: e.innerText,
+
            }).catch(console.error);
+
          };
+
          void invoke<Uint8Array>("get_embed", {
+
            rid,
+
            oid: href,
+
          })
+
            .then(byteArray => {
+
              const buffer = Buffer.from(byteArray);
+
              const blob = new Blob([buffer]);
+
              const url = URL.createObjectURL(blob);
+
              const ext = e.innerHTML.split(".").at(-1);
+
              // Embed an img element below the link
+
              if (ext?.match(/(gif|jpe?g|tiff?|png|webp|bmp)/)) {
+
                const element = document.createElement("img");
+
                element.setAttribute("src", url);
+
                e.insertAdjacentElement("afterend", element);
+
                // Embed an iframe to display pdf correctly element below the link
+
              } else if (ext?.match(/(pdf)/)) {
+
                const element = document.createElement("embed");
+
                element.setAttribute("src", url);
+
                element.type = "application/pdf";
+
                element.style.overflow = "scroll";
+
                element.style.height = "40rem";
+
                element.style.overscrollBehavior = "contain";
+
                e.insertAdjacentElement("afterend", element);
+
              } else if (ext?.match(/(mp4|mov)/)) {
+
                const element = document.createElement("video");
+
                const node = document.createElement("source");
+
                node.src = url;
+
                element.controls = true;
+
                node.type = `video/mp4`;
+
                element.style.width = "100%";
+
                element.appendChild(node);
+
                e.insertAdjacentElement("afterend", element);
+
              } else if (ext?.match(/(mp3)/)) {
+
                const element = document.createElement("audio");
+
                element.src = url;
+
                element.controls = true;
+
                e.insertAdjacentElement("afterend", element);
+
              } else {
+
                console.warn(
+
                  `Not able to provide a preview for ${e.innerHTML}`,
+
                );
+
              }
+
            })
+
            .catch(console.error);
+
        }
      }

      // Replace standard HTML checkboxes with our custom radicle-icon-small element
@@ -75,22 +138,6 @@
        i.remove();
      }

-
      // Iterate over all images, and replace the source with a canonicalized URL
-
      // pointing at the repos /raw endpoint.
-
      for (const i of container.querySelectorAll("img")) {
-
        const imagePath = i.getAttribute("src");
-

-
        // If the image is an oid embed
-
        if (imagePath && isCommit(imagePath)) {
-
          void invoke<string>("get_file_by_oid", {
-
            rid,
-
            oid: imagePath,
-
          }).then(base64Content =>
-
            i.setAttribute("src", `data:image/jpeg;base64,${base64Content}`),
-
          );
-
        }
-
      }
-

      // Replaces code blocks in the background with highlighted code.
      const prefix = "language-";
      const nodes = Array.from(document.body.querySelectorAll("pre code"));
modified src/components/Textarea.svelte
@@ -1,15 +1,18 @@
<script lang="ts">
-
  import type { FormEventHandler } from "svelte/elements";
+
  import type {
+
    ClipboardEventHandler,
+
    FormEventHandler,
+
  } from "svelte/elements";
  import type { ComponentProps } from "svelte";

-
  import { tick } from "svelte";
-

+
  import { onMount, tick } from "svelte";
  import * as utils from "@app/lib/utils";
-

  import Border from "./Border.svelte";

  interface Props {
+
    draggingOver?: boolean;
    borderVariant?: ComponentProps<typeof Border>["variant"];
+
    onpaste?: ClipboardEventHandler<HTMLTextAreaElement>;
    focus?: boolean;
    oninput?: FormEventHandler<HTMLTextAreaElement>;
    onkeypress?: FormEventHandler<HTMLTextAreaElement>;
@@ -25,8 +28,10 @@

  /* eslint-disable prefer-const */
  let {
+
    draggingOver,
    borderVariant = "float",
    focus = false,
+
    onpaste,
    oninput,
    onkeypress,
    placeholder = undefined,
@@ -44,6 +49,28 @@
  let textareaElement: HTMLTextAreaElement | undefined = $state(undefined);
  let focussed = $state(false);

+
  onMount(() => {
+
    if (textareaElement) {
+
      // The selectionchange event listener doesn't modify the selection on Enter.
+
      textareaElement.addEventListener("keydown", (event: KeyboardEvent) => {
+
        if (event.key === "Enter") {
+
          selectionStart += 1;
+
          selectionEnd += 1;
+
        }
+
      });
+
      textareaElement.addEventListener("selectionchange", (event: Event) => {
+
        if (
+
          event.target &&
+
          "selectionStart" in event.target &&
+
          "selectionEnd" in event.target
+
        ) {
+
          selectionStart = event.target.selectionStart as number;
+
          selectionEnd = event.target.selectionEnd as number;
+
        }
+
      });
+
    }
+
  });
+

  // We either auto-grow the textarea, or allow the user to resize it. These
  // options are mutually exclusive because a user resized textarea would
  // automatically shrink upon text input otherwise.
@@ -67,12 +94,6 @@
    }
  });

-
  $effect.pre(() => {
-
    if (textareaElement) {
-
      ({ selectionStart, selectionEnd } = textareaElement);
-
    }
-
  });
-

  $effect(() => {
    void tick().then(() => {
      if (textareaElement && focus) {
@@ -123,10 +144,22 @@
  textarea::placeholder {
    color: var(--color-foreground-dim);
  }
+

+
  .dragover {
+
    position: absolute;
+
    opacity: 0.5;
+
    display: flex;
+
    justify-content: center;
+
    align-items: center;
+
    width: 100%;
+
    height: 100%;
+
    background-color: var(--color-background-float);
+
  }
</style>

<Border
  variant={focussed ? "secondary" : borderVariant}
+
  stylePosition="relative"
  styleWidth="100%"
  {styleMinHeight}>
  <textarea
@@ -142,10 +175,14 @@
      ? "scroll"
      : undefined}
    {placeholder}
+
    {onpaste}
    {oninput}
    {onkeypress}
    onfocus={() => (focussed = true)}
    onblur={() => (focussed = false)}
    onkeydown={handleKeydown}>
  </textarea>
+
  {#if draggingOver}
+
    <div class="txt-small dragover">Drop files to add them as embeds.</div>
+
  {/if}
</Border>
deleted src/lib/file.ts
@@ -1,38 +0,0 @@
-
async function parseGitOid(bytes: Uint8Array): Promise<string> {
-
  // Create the header
-
  const header = new TextEncoder().encode(`blob ${bytes.length}\0`);
-

-
  // Concatenate the header and the original file content
-
  const combined = new Uint8Array(header.length + bytes.length);
-
  combined.set(header);
-
  combined.set(bytes, header.length);
-

-
  // Compute the SHA-1 hash
-
  const hashBuffer = await crypto.subtle.digest("SHA-1", combined);
-
  const hashArray = Array.from(new Uint8Array(hashBuffer));
-

-
  // Convert the hash to a hexadecimal string
-
  return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
-
}
-

-
function base64String(file: File): Promise<string> {
-
  return new Promise((resolve, reject) => {
-
    const reader = new FileReader();
-
    reader.onload = (event: ProgressEvent<FileReader>) => {
-
      if (event.target?.result && typeof event.target.result === "string") {
-
        resolve(event.target.result);
-
      } else {
-
        reject(new Error("Failed to generate base64 string"));
-
      }
-
    };
-

-
    reader.readAsDataURL(file);
-
  });
-
}
-

-
export async function embed(file: File) {
-
  const bytes = new Uint8Array(await file.arrayBuffer());
-
  const oid = await parseGitOid(bytes);
-
  const content = await base64String(file);
-
  return { oid, name: file.name, content };
-
}
modified src/views/repo/CreateIssue.svelte
@@ -4,6 +4,7 @@
  import type { Issue } from "@bindings/cob/issue/Issue";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
  import type { IssueStatus } from "./router";
+
  import type { Embed } from "@bindings/cob/thread/Embed";

  import { invoke } from "@app/lib/invoke";

@@ -14,18 +15,14 @@

  import AssigneeInput from "@app/components/AssigneeInput.svelte";
  import Border from "@app/components/Border.svelte";
-
  import Button from "@app/components/Button.svelte";
-
  import Icon from "@app/components/Icon.svelte";
  import InlineTitle from "@app/components/InlineTitle.svelte";
  import IssueSecondColumn from "@app/components/IssueSecondColumn.svelte";
  import LabelInput from "@app/components/LabelInput.svelte";
-
  import Markdown from "@app/components/Markdown.svelte";
-
  import OutlineButton from "@app/components/OutlineButton.svelte";
  import Sidebar from "@app/components/Sidebar.svelte";
  import TextInput from "@app/components/TextInput.svelte";
-
  import Textarea from "@app/components/Textarea.svelte";

  import Layout from "./Layout.svelte";
+
  import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";

  interface Props {
    repo: RepoInfo;
@@ -36,17 +33,17 @@

  const { repo, issues, config, status }: Props = $props();

-
  let description: string = $state("");
  let preview: boolean = $state(false);
  let title: string = $state("");

-
  const embeds: { name: string; content: string }[] = [];
-

  let assignees: Author[] = $state([]);
  let labels: string[] = $state([]);

-
  async function createIssue() {
-
    const response: Issue = await invoke("create_issue", {
+
  async function createIssue(
+
    description: string,
+
    embeds: Embed[],
+
  ): Promise<Issue> {
+
    return invoke("create_issue", {
      rid: repo.rid,
      new: {
        title,
@@ -57,12 +54,6 @@
      },
      opts: { announce: $nodeRunning && $announce },
    });
-
    void router.push({
-
      resource: "repo.issue",
-
      rid: repo.rid,
-
      issue: response.id,
-
      status,
-
    });
  }
</script>

@@ -79,12 +70,6 @@
    padding: 0 1rem 1rem 1rem;
    height: calc(100% - 14rem);
  }
-
  .body {
-
    background-color: var(--color-background-float);
-
    padding: 1rem;
-
    min-height: calc(100% + 2px);
-
    clip-path: var(--2px-corner-fill);
-
  }
  .metadata-divider {
    width: 2px;
    background-color: var(--color-fill-ghost);
@@ -148,55 +133,29 @@
      </Border>
    </div>

-
    {#if preview}
-
      <div class="txt-small body">
-
        {#if description.trim() === ""}
-
          <span class="txt-missing" style:line-height="1.625rem">
-
            No description.
-
          </span>
-
        {:else}
-
          <Markdown rid={repo.rid} content={description} breaks />
-
        {/if}
-
      </div>
-
    {:else}
-
      <Textarea
-
        borderVariant="ghost"
-
        placeholder="Description"
-
        bind:value={description}
-
        size="fixed-height"
-
        submit={createIssue}
-
        styleMinHeight="100%" />
-
    {/if}
-
    <div
-
      class="global-flex"
-
      style:justify-content="space-between"
-
      style:padding-bottom="1.5rem"
-
      style:margin-top="1.5rem">
-
      <OutlineButton
-
        variant="ghost"
-
        onclick={() => {
-
          window.history.back();
-
        }}>
-
        <Icon name="cross" />Discard
-
      </OutlineButton>
-
      <div class="global-flex">
-
        <div class="global-flex txt-small txt-missing">
-
          <Icon name="markdown" />
-
          Markdown is supported.
-
        </div>
-
        <OutlineButton
-
          variant="ghost"
-
          disabled={title.length === 0}
-
          onclick={() => (preview = !preview)}>
-
          <Icon name={preview ? "pen" : "eye"} />{preview ? "Edit" : "Preview"}
-
        </OutlineButton>
-
        <Button
-
          variant="ghost"
-
          disabled={title.length === 0}
-
          onclick={createIssue}>
-
          <Icon name="checkmark" />Save
-
        </Button>
-
      </div>
-
    </div>
+
    <ExtendedTextarea
+
      submitCaption="Save"
+
      close={() => window.history.back()}
+
      submit={async ({ comment, embeds }) => {
+
        try {
+
          const response = await createIssue(
+
            comment,
+
            Array.from(embeds.values()),
+
          );
+
          void router.push({
+
            resource: "repo.issue",
+
            rid: repo.rid,
+
            issue: response.id,
+
            status,
+
          });
+
        } catch {
+
          console.error("Not able to create issue.");
+
        }
+
      }}
+
      styleMinHeight="100%"
+
      rid={repo.rid}
+
      bind:preview
+
      borderVariant="ghost"
+
      placeholder="Description" />
  </div>
</Layout>
modified tests/e2e/repo/issue.spec.ts
@@ -41,7 +41,7 @@ test("creation of top level comments", async ({ page }) => {
    .fill(
      "It's important for us that the comment creation flow works as expected.",
    );
-
  await page.getByRole("button", { name: "Save" }).click();
+
  await page.getByRole("button", { name: "icon-checkmark" }).click();
  await expect(
    page.getByText("Make sure that comment creation is working").last(),
  ).toBeVisible();
@@ -60,7 +60,7 @@ test("creation of top level comments", async ({ page }) => {
  await page
    .getByPlaceholder("Leave a comment")
    .fill("A top level comment by playwright");
-
  await page.getByRole("button", { name: "icon-checkmark Comment" }).click();
+
  await page.getByRole("button", { name: "icon-checkmark" }).click();
  await expect(
    page.getByText("A top level comment by playwright"),
  ).toBeVisible();
@@ -71,7 +71,7 @@ test("creation of top level comments", async ({ page }) => {
    .fill(
      "A top level comment by playwright created by replying to the issue body",
    );
-
  await page.getByRole("button", { name: "icon-checkmark Comment" }).click();
+
  await page.getByRole("button", { name: "icon-checkmark" }).click();
  await expect(
    page.getByText(
      "A top level comment by playwright created by replying to the issue body",
@@ -82,7 +82,7 @@ test("creation of top level comments", async ({ page }) => {
  await page
    .getByPlaceholder("Reply to comment")
    .fill("A reply comment by playwright replying to the first comment");
-
  await page.getByRole("button", { name: "icon-checkmark Reply" }).click();
+
  await page.getByRole("button", { name: "icon-checkmark" }).click();
  await expect(
    page.getByText(
      "A reply comment by playwright replying to the first comment",