diff --git a/apps/staged/package.json b/apps/staged/package.json index 9b8ffbac..9d7e0e50 100644 --- a/apps/staged/package.json +++ b/apps/staged/package.json @@ -17,6 +17,7 @@ "@tauri-apps/cli": "^2.10.0", "@tsconfig/svelte": "^5.0.6", "@types/node": "^24.10.1", + "@types/sanitize-html": "^2.13.0", "prettier": "^3.7.4", "prettier-plugin-svelte": "^3.4.1", "svelte": "^5.46.4", @@ -25,6 +26,13 @@ "vite": "^7.2.4" }, "dependencies": { - "@tauri-apps/api": "^2.10.0" + "@builderbot/diff-viewer": "workspace:*", + "@tauri-apps/api": "^2.10.0", + "@tauri-apps/plugin-dialog": "^2.2.0", + "@tauri-apps/plugin-store": "^2.2.0", + "lucide-svelte": "^0.575.0", + "marked": "^17.0.1", + "sanitize-html": "^2.17.0", + "shiki": "^3.20.0" } } diff --git a/apps/staged/src-tauri/Cargo.lock b/apps/staged/src-tauri/Cargo.lock index 557404c8..26e2afb1 100644 --- a/apps/staged/src-tauri/Cargo.lock +++ b/apps/staged/src-tauri/Cargo.lock @@ -2,58 +2,12 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "acp-client" -version = "0.1.0" -dependencies = [ - "agent-client-protocol", - "anyhow", - "async-trait", - "blox-cli", - "log", - "serde", - "serde_json", - "tokio", - "tokio-util", -] - [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "agent-client-protocol" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2659b1089101b15db31137710159421cb44785ecdb5ba784be3b4a6f8cb8a475" -dependencies = [ - "agent-client-protocol-schema", - "anyhow", - "async-broadcast", - "async-trait", - "derive_more 2.1.1", - "futures", - "log", - "serde", - "serde_json", -] - -[[package]] -name = "agent-client-protocol-schema" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bc1fef9c32f03bce2ab44af35b6f483bfd169bf55cc59beeb2e3b1a00ae4d1" -dependencies = [ - "anyhow", - "derive_more 2.1.1", - "schemars 1.2.1", - "serde", - "serde_json", - "strum", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -93,29 +47,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[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-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" @@ -196,16 +127,6 @@ dependencies = [ "objc2", ] -[[package]] -name = "blox-cli" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "thiserror 2.0.18", - "wait-timeout", -] - [[package]] name = "brotli" version = "8.0.2" @@ -227,20 +148,6 @@ dependencies = [ "alloc-stdlib", ] -[[package]] -name = "builderbot-actions" -version = "0.1.0" -dependencies = [ - "acp-client", - "anyhow", - "async-trait", - "libc", - "serde", - "serde_json", - "tokio", - "uuid", -] - [[package]] name = "bumpalo" version = "3.20.2" @@ -342,6 +249,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -400,30 +309,12 @@ dependencies = [ "memchr", ] -[[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 = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "cookie" version = "0.18.1" @@ -605,7 +496,7 @@ version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "convert_case 0.4.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -613,45 +504,43 @@ dependencies = [ ] [[package]] -name = "derive_more" -version = "2.1.1" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "derive_more-impl", + "block-buffer", + "crypto-common", ] [[package]] -name = "derive_more-impl" -version = "2.1.1" +name = "dirs" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "convert_case 0.10.0", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", - "unicode-xid", + "dirs-sys 0.4.1", ] [[package]] -name = "digest" -version = "0.10.7" +name = "dirs" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "block-buffer", - "crypto-common", + "dirs-sys 0.5.0", ] [[package]] -name = "dirs" -version = "6.0.0" +name = "dirs-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ - "dirs-sys", + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] @@ -662,7 +551,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -679,6 +568,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.11.0", + "block2", + "libc", "objc2", ] @@ -789,37 +680,6 @@ dependencies = [ "typeid", ] -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - [[package]] name = "fdeflate" version = "0.3.7" @@ -913,21 +773,6 @@ dependencies = [ "new_debug_unreachable", ] -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.32" @@ -935,7 +780,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", - "futures-sink", ] [[package]] @@ -990,7 +834,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1198,6 +1041,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "git-diff" +version = "0.1.0" +dependencies = [ + "git2", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "git2" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +dependencies = [ + "bitflags 2.11.0", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + [[package]] name = "glib" version = "0.18.5" @@ -1695,6 +1562,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.88" @@ -1792,6 +1669,20 @@ version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libgit2-sys" +version = "0.18.3+1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -1812,6 +1703,32 @@ dependencies = [ "libc", ] +[[package]] +name = "libssh2-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "litemap" version = "0.8.1" @@ -2230,6 +2147,24 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2261,12 +2196,6 @@ dependencies = [ "system-deps", ] -[[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.5" @@ -2702,6 +2631,17 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -2796,6 +2736,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -2828,7 +2792,7 @@ checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ "dyn-clone", "indexmap 1.9.3", - "schemars_derive 0.8.22", + "schemars_derive", "serde", "serde_json", "url", @@ -2855,7 +2819,6 @@ checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", - "schemars_derive 1.2.1", "serde", "serde_json", ] @@ -2872,18 +2835,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "schemars_derive" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.117", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -2898,7 +2849,7 @@ checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", "cssparser", - "derive_more 0.99.20", + "derive_more", "fxhash", "log", "phf 0.8.0", @@ -3093,16 +3044,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - [[package]] name = "simd-adler32" version = "0.3.8" @@ -3201,13 +3142,14 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" name = "staged" version = "0.1.0" dependencies = [ - "acp-client", - "blox-cli", - "builderbot-actions", + "dirs 5.0.1", + "git-diff", "serde", "serde_json", "tauri", "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-store", ] [[package]] @@ -3241,27 +3183,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "swift-rs" version = "1.0.7" @@ -3394,7 +3315,7 @@ dependencies = [ "anyhow", "bytes", "cookie", - "dirs", + "dirs 6.0.0", "dunce", "embed_plist", "getrandom 0.3.4", @@ -3444,7 +3365,7 @@ checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74" dependencies = [ "anyhow", "cargo_toml", - "dirs", + "dirs 6.0.0", "glob", "heck 0.5.0", "json-patch", @@ -3499,6 +3420,79 @@ dependencies = [ "tauri-utils", ] +[[package]] +name = "tauri-plugin" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692a77abd8b8773e107a42ec0e05b767b8d2b7ece76ab36c6c3947e34df9f53f" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-store" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca1a8ff83c269b115e98726ffc13f9e548a10161544a92ad121d6d0a96e16ea" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "tauri-runtime" version = "2.10.0" @@ -3702,7 +3696,6 @@ dependencies = [ "libc", "mio", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -3727,7 +3720,6 @@ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", - "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -3881,9 +3873,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -3900,7 +3904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "libappindicator", "muda", "objc2", @@ -4041,6 +4045,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.1" @@ -4073,15 +4083,6 @@ dependencies = [ "libc", ] -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -4521,6 +4522,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -4563,6 +4573,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -4620,6 +4645,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4638,6 +4669,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4656,6 +4693,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4686,6 +4729,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4704,6 +4753,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4722,6 +4777,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4740,6 +4801,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4884,7 +4951,7 @@ dependencies = [ "block2", "cookie", "crossbeam-channel", - "dirs", + "dirs 6.0.0", "dpi", "dunce", "gdkx11", diff --git a/apps/staged/src-tauri/Cargo.toml b/apps/staged/src-tauri/Cargo.toml index dabb7351..0e9519a6 100644 --- a/apps/staged/src-tauri/Cargo.toml +++ b/apps/staged/src-tauri/Cargo.toml @@ -19,8 +19,9 @@ tauri-build = { version = "2.5.5", features = [] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tauri = { version = "2.10.2", features = [] } +tauri-plugin-dialog = "2" +tauri-plugin-store = "2" +dirs = "5.0" -# Shared crates (proves the split works) -builderbot-actions = { path = "../../../crates/builderbot-actions" } -acp-client = { path = "../../../crates/acp-client" } -blox-cli = { path = "../../../crates/blox-cli" } +# Shared crate for diff computation +git-diff = { path = "../../../crates/git-diff" } diff --git a/apps/staged/src-tauri/capabilities/default.json b/apps/staged/src-tauri/capabilities/default.json index 8e906f70..c74bb83a 100644 --- a/apps/staged/src-tauri/capabilities/default.json +++ b/apps/staged/src-tauri/capabilities/default.json @@ -3,5 +3,11 @@ "identifier": "default", "description": "enables the default permissions", "windows": ["main"], - "permissions": ["core:default"] + "permissions": [ + "core:default", + "core:window:allow-start-dragging", + "dialog:default", + "dialog:allow-open", + "store:default" + ] } diff --git a/apps/staged/src-tauri/src/lib.rs b/apps/staged/src-tauri/src/lib.rs index da7f6a5a..37100434 100644 --- a/apps/staged/src-tauri/src/lib.rs +++ b/apps/staged/src-tauri/src/lib.rs @@ -1,14 +1,658 @@ //! Staged — standalone diff viewer. +//! +//! A focused diff viewer that opens a git repository and shows diffs +//! using the shared git-diff crate and @builderbot/diff-viewer package. +use serde::Serialize; +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::Mutex; + +// ============================================================================= +// App state +// ============================================================================= + +struct AppState { + repo_path: PathBuf, +} + +// ============================================================================= +// Types +// ============================================================================= + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct RepoInfo { + path: String, + branch: String, + default_branch: String, + commits_ahead: u32, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct CommitInfo { + sha: String, + short_sha: String, + message: String, + author: String, + timestamp: i64, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct DiffFilesResponse { + files: Vec, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct LaunchArgs { + repo_path: String, + mode: Option, + commit: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct DirEntry { + name: String, + path: String, + is_dir: bool, + is_repo: bool, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct RecentRepo { + name: String, + path: String, +} + +// ============================================================================= +// Commands: Git info +// ============================================================================= + +#[tauri::command(rename_all = "camelCase")] +fn get_repo_info(state: tauri::State<'_, Mutex>) -> Result { + let state = state.lock().unwrap(); + let repo = &state.repo_path; + + let branch = run_git(repo, &["rev-parse", "--abbrev-ref", "HEAD"]) + .unwrap_or_else(|_| "HEAD".to_string()); + + let default_branch = + git_diff::detect_default_branch(repo).unwrap_or_else(|_| "origin/main".to_string()); + + let commits_ahead = run_git( + repo, + &["rev-list", "--count", &format!("{default_branch}..HEAD")], + ) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); + + Ok(RepoInfo { + path: repo.display().to_string(), + branch: branch.trim().to_string(), + default_branch, + commits_ahead, + }) +} + +#[tauri::command(rename_all = "camelCase")] +fn list_recent_commits( + state: tauri::State<'_, Mutex>, + count: Option, +) -> Result, String> { + let state = state.lock().unwrap(); + let repo = &state.repo_path; + let count = count.unwrap_or(20); + + let output = run_git( + repo, + &[ + "log", + &format!("-{count}"), + "--format=%H%n%h%n%s%n%an%n%at", + "--no-merges", + ], + ) + .map_err(|e| e.to_string())?; + + let mut commits = Vec::new(); + let lines: Vec<&str> = output.lines().collect(); + + for chunk in lines.chunks(5) { + if chunk.len() == 5 { + commits.push(CommitInfo { + sha: chunk[0].to_string(), + short_sha: chunk[1].to_string(), + message: chunk[2].to_string(), + author: chunk[3].to_string(), + timestamp: chunk[4].parse().unwrap_or(0), + }); + } + } + + Ok(commits) +} + +// ============================================================================= +// Commands: Diff operations +// ============================================================================= + +#[tauri::command(rename_all = "camelCase")] +fn list_diff_files( + state: tauri::State<'_, Mutex>, + spec: git_diff::DiffSpec, +) -> Result { + let state = state.lock().unwrap(); + let repo = &state.repo_path; + + let files = git_diff::list_diff_files(repo, &spec).map_err(|e| e.to_string())?; + Ok(DiffFilesResponse { files }) +} + +#[tauri::command(rename_all = "camelCase")] +fn get_file_diff( + state: tauri::State<'_, Mutex>, + spec: git_diff::DiffSpec, + path: String, +) -> Result { + let state = state.lock().unwrap(); + let repo = &state.repo_path; + let file_path = Path::new(&path); + + git_diff::get_file_diff(repo, &spec, file_path).map_err(|e| e.to_string()) +} + +#[tauri::command(rename_all = "camelCase")] +fn get_file_at_ref( + state: tauri::State<'_, Mutex>, + ref_name: String, + path: String, +) -> Result { + let state = state.lock().unwrap(); + let repo = &state.repo_path; + + git_diff::get_file_at_ref(repo, &ref_name, &path).map_err(|e| e.to_string()) +} + +// ============================================================================= +// Commands: Launch args +// ============================================================================= + +#[tauri::command(rename_all = "camelCase")] +fn get_launch_args(state: tauri::State<'_, Mutex>) -> LaunchArgs { + let state = state.lock().unwrap(); + + let args: Vec = std::env::args().collect(); + let mut mode: Option = None; + let mut commit: Option = None; + + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--staged" | "-s" => mode = Some("staged".to_string()), + "--branch" | "-b" => mode = Some("branch".to_string()), + "--commit" | "-c" => { + mode = Some("commit".to_string()); + if i + 1 < args.len() && !args[i + 1].starts_with('-') { + i += 1; + commit = Some(args[i].clone()); + } + } + "--all" | "-a" => mode = Some("all".to_string()), + _ => {} + } + i += 1; + } + + LaunchArgs { + repo_path: state.repo_path.display().to_string(), + mode, + commit, + } +} + +// ============================================================================= +// Commands: Repo selection +// ============================================================================= + +#[tauri::command(rename_all = "camelCase")] +fn set_repo_path(state: tauri::State<'_, Mutex>, path: String) -> Result<(), String> { + let p = PathBuf::from(&path); + if !p.join(".git").exists() && run_git(&p, &["rev-parse", "--git-dir"]).is_err() { + return Err(format!("{path} is not a git repository")); + } + let mut s = state.lock().unwrap(); + s.repo_path = p; + Ok(()) +} + +// ============================================================================= +// Commands: Directory browsing +// ============================================================================= + +/// List contents of a directory. +/// Returns directories first (sorted), then files (sorted). +/// Hidden files (starting with .) are excluded. +#[tauri::command(rename_all = "camelCase")] +fn list_directory(path: String) -> Result, String> { + let dir = Path::new(&path); + + if !dir.exists() { + return Err(format!("Directory does not exist: {path}")); + } + if !dir.is_dir() { + return Err(format!("Not a directory: {path}")); + } + + let mut dirs = Vec::new(); + let mut files = Vec::new(); + + let entries = std::fs::read_dir(dir).map_err(|e| format!("Failed to read directory: {e}"))?; + + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with('.') { + continue; + } + + let entry_path = entry.path(); + let is_dir = entry_path.is_dir(); + let is_repo = is_dir && entry_path.join(".git").exists(); + + let item = DirEntry { + name, + path: entry_path.to_string_lossy().to_string(), + is_dir, + is_repo, + }; + + if is_dir { + dirs.push(item); + } else { + files.push(item); + } + } + + dirs.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + files.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + dirs.extend(files); + Ok(dirs) +} + +/// Folders to skip during search. +const SKIP_FOLDERS: &[&str] = &[ + "Library", + "Applications", + "System", + "Volumes", + "cores", + "private", + "node_modules", + "target", + "build", + "dist", + "vendor", + ".git", + "__pycache__", + "venv", + ".venv", + "env", + ".cargo", + ".rustup", + ".npm", + ".cache", + "Caches", + "Movies", + "Music", + "Pictures", + "Photos Library.photoslibrary", +]; + +/// Common development folder names. +const DEV_FOLDERS: &[&str] = &[ + "dev", + "projects", + "code", + "repos", + "src", + "workspace", + "work", + "github", + "gitlab", + "Development", + "Documents", + "Desktop", +]; + +/// Search for git repositories matching a query. +#[tauri::command(rename_all = "camelCase")] +fn search_directories( + path: String, + query: String, + max_depth: Option, + limit: Option, +) -> Result, String> { + let dir = Path::new(&path); + let max_depth = max_depth.unwrap_or(6); + let limit = limit.unwrap_or(20); + let query_lower = query.to_lowercase(); + + if !dir.exists() || !dir.is_dir() { + return Err(format!("Invalid directory: {path}")); + } + + let mut results = Vec::new(); + let collect_limit = limit * 3; + + let home_dir = dirs::home_dir(); + let is_home = home_dir.as_ref().is_some_and(|h| h == dir); + + if is_home { + for dev_folder in DEV_FOLDERS { + let dev_path = dir.join(dev_folder); + if dev_path.exists() && dev_path.is_dir() { + search_repos_recursive( + &dev_path, + &query_lower, + 0, + max_depth, + &mut results, + collect_limit, + ); + if results.len() >= collect_limit { + break; + } + } + } + } else { + search_repos_recursive(dir, &query_lower, 0, max_depth, &mut results, collect_limit); + } + + results.sort_by(|a, b| { + let a_exact = a.name.to_lowercase() == query_lower; + let b_exact = b.name.to_lowercase() == query_lower; + if a_exact != b_exact { + return b_exact.cmp(&a_exact); + } + let a_depth = a.path.matches('/').count(); + let b_depth = b.path.matches('/').count(); + a_depth.cmp(&b_depth) + }); + results.truncate(limit); + + Ok(results) +} + +fn search_repos_recursive( + dir: &Path, + query: &str, + depth: u32, + max_depth: u32, + results: &mut Vec, + limit: usize, +) -> bool { + if depth > max_depth || results.len() >= limit { + return results.len() >= limit; + } + + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return false, + }; + + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with('.') { + continue; + } + if SKIP_FOLDERS.contains(&name.as_str()) { + continue; + } + + let entry_path = entry.path(); + if !entry_path.is_dir() { + continue; + } + + let is_repo = entry_path.join(".git").exists(); + + if is_repo { + let name_lower = name.to_lowercase(); + if query.is_empty() || name_lower.starts_with(query) || name_lower.contains(query) { + results.push(DirEntry { + name: name.clone(), + path: entry_path.to_string_lossy().to_string(), + is_dir: true, + is_repo: true, + }); + if results.len() >= limit { + return true; + } + } + } else if search_repos_recursive(&entry_path, query, depth + 1, max_depth, results, limit) { + return true; + } + } + + false +} + +/// Get the user's home directory. #[tauri::command] -fn greet(name: &str) -> String { - format!("Hello, {}! Welcome to Staged.", name) +fn get_home_dir() -> Result { + dirs::home_dir() + .map(|p| p.to_string_lossy().to_string()) + .ok_or_else(|| "Could not determine home directory".to_string()) +} + +// ============================================================================= +// Commands: Recent repos (Spotlight) +// ============================================================================= + +/// Directories to scan for recent activity. +const SCAN_DIRS: &[&str] = &[ + "Documents", + "Downloads", + "Desktop", + "Development", + "dev", + "projects", + "code", + "repos", + "src", + "workspace", + "work", + "github", + "gitlab", +]; + +/// Paths to exclude from results. +const EXCLUDE_PATTERNS: &[&str] = &[ + "node_modules", + "/target/", + "/.git/", + "/.cargo/", + "/.rustup/", + "/Library/", + "/.Trash/", + "/__pycache__/", + "/venv/", + "/.venv/", +]; + +#[tauri::command(rename_all = "camelCase")] +fn find_recent_repos(hours_ago: Option, limit: Option) -> Vec { + let hours_ago = hours_ago.unwrap_or(24); + let limit = limit.unwrap_or(10); + + let home = match dirs::home_dir() { + Some(h) => h, + None => return Vec::new(), + }; + + let scan_dirs: Vec = SCAN_DIRS + .iter() + .map(|d| home.join(d)) + .filter(|p| p.exists()) + .collect(); + + if scan_dirs.is_empty() { + return Vec::new(); + } + + let files = match find_recent_files_mdfind(&scan_dirs, hours_ago) { + Some(f) => f, + None => return Vec::new(), + }; + + let mut seen_repos: HashSet = HashSet::new(); + let mut repos: Vec = Vec::new(); + + for file in files { + if EXCLUDE_PATTERNS.iter().any(|p| file.contains(p)) { + continue; + } + + if let Some(repo_path) = find_git_root(Path::new(&file), &home) { + if seen_repos.insert(repo_path.clone()) { + let name = repo_path + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "Repository".to_string()); + + repos.push(RecentRepo { + name, + path: repo_path.to_string_lossy().to_string(), + }); + + if repos.len() >= limit { + break; + } + } + } + } + + repos } +fn find_recent_files_mdfind(scan_dirs: &[PathBuf], hours_ago: u32) -> Option> { + let seconds = hours_ago * 3600; + + let mut args: Vec = Vec::new(); + for dir in scan_dirs { + args.push("-onlyin".to_string()); + args.push(dir.to_string_lossy().to_string()); + } + + args.push(format!( + "kMDItemFSContentChangeDate >= $time.now(-{seconds})" + )); + + let output = Command::new("mdfind").args(&args).output().ok()?; + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let files: Vec = stdout + .lines() + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + + Some(files) +} + +fn find_git_root(path: &Path, home: &Path) -> Option { + let mut current = if path.is_file() { + path.parent()?.to_path_buf() + } else { + path.to_path_buf() + }; + + while current.starts_with(home) && current != *home { + if current.join(".git").exists() { + return Some(current); + } + current = current.parent()?.to_path_buf(); + } + + None +} + +// ============================================================================= +// Helpers +// ============================================================================= + +fn run_git(repo: &Path, args: &[&str]) -> Result { + let output = std::process::Command::new("git") + .args(["-C", &repo.display().to_string()]) + .args(args) + .output() + .map_err(|e| e.to_string())?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(stderr.into_owned()); + } + + String::from_utf8(output.stdout).map_err(|e| e.to_string()) +} + +fn resolve_repo_path() -> PathBuf { + let args: Vec = std::env::args().collect(); + let mut iter = args.iter().skip(1); + + while let Some(arg) = iter.next() { + match arg.as_str() { + "--commit" | "-c" => { + iter.next(); + } + s if s.starts_with('-') => {} + path => { + let p = PathBuf::from(path); + if p.exists() { + return std::fs::canonicalize(&p).unwrap_or(p); + } + } + } + } + + std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) +} + +// ============================================================================= +// App entry point +// ============================================================================= + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + let repo_path = resolve_repo_path(); + tauri::Builder::default() - .invoke_handler(tauri::generate_handler![greet]) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_store::Builder::default().build()) + .manage(Mutex::new(AppState { repo_path })) + .invoke_handler(tauri::generate_handler![ + get_repo_info, + list_recent_commits, + list_diff_files, + get_file_diff, + get_file_at_ref, + get_launch_args, + set_repo_path, + list_directory, + search_directories, + get_home_dir, + find_recent_repos, + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/apps/staged/src-tauri/tauri.conf.json b/apps/staged/src-tauri/tauri.conf.json index b0247abe..981f2941 100644 --- a/apps/staged/src-tauri/tauri.conf.json +++ b/apps/staged/src-tauri/tauri.conf.json @@ -12,9 +12,13 @@ "app": { "windows": [ { - "title": "Staged — Diff Viewer", - "width": 1200, - "height": 800 + "title": "Staged", + "width": 1400, + "height": 900, + "minWidth": 900, + "minHeight": 600, + "titleBarStyle": "Overlay", + "hiddenTitle": true } ], "security": { diff --git a/apps/staged/src/App.svelte b/apps/staged/src/App.svelte index 7754f586..4d245322 100644 --- a/apps/staged/src/App.svelte +++ b/apps/staged/src/App.svelte @@ -1,26 +1,1378 @@ -
-

Staged

-

Diff Viewer — placeholder application.

-
+ + + +{#if initialized} +
+ + +
+
+ +
+
+ + + + +
+ + + {#if showCommitPicker} +
+ {#if loadingCommits} +
+ + Loading commits... +
+ {:else if commits.length === 0} +
No commits found
+ {:else} + {#each commits as commit (commit.sha)} + + {/each} + {/if} +
+ {/if} +
+
+ + {#if files.length > 0} + {files.length} file{files.length === 1 ? '' : 's'} + {/if} +
+ +
+ {#if repoInfo} + + + {repoInfo.branch} + + + {/if} + +
+
+ + +
+
+ {#if loading} +
+ + Loading diff... +
+ {:else if error} +
+ {error} +
+ {:else if repoError} +
+ {repoError} + +
+ {:else if files.length === 0} +
+
+
+ +
+

No changes found

+

Choose a change set from the toolbar above

+
+
+ +
+
+ {:else} + c.path === selectedFile)} + loading={loadingFile !== null} + beforeLabel="before" + afterLabel="after" + onAddComment={handleAddComment} + onUpdateComment={handleUpdateComment} + onDeleteComment={handleDeleteComment} + /> + {/if} +
+ + {#if files.length > 0} +
+ +
+ {/if} +
+
+ + + {#if showFolderPicker} + (showFolderPicker = false)} + /> + {/if} + + {#if showThemePicker} + (showThemePicker = false)} /> + {/if} +{/if} diff --git a/apps/staged/src/app.css b/apps/staged/src/app.css index 19fc4a69..927517b0 100644 --- a/apps/staged/src/app.css +++ b/apps/staged/src/app.css @@ -1,10 +1,103 @@ +* { + box-sizing: border-box; +} + :root { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - color: #e0e0e0; - background-color: #1a1a2e; + --bg-primary: #27212e; + --bg-chrome: #18151d; + --bg-deepest: #020203; + --bg-elevated: #38333f; + --bg-hover: #342e3b; + + --border-subtle: #38333f; + --border-muted: #47424d; + --border-emphasis: #5d5962; + + --text-primary: #ffffff; + --text-muted: #91889b; + --text-faint: #5c5565; + --text-accent: #58a6ff; + + --status-modified: #d29922; + --status-added: #3fb950; + --status-deleted: #f85149; + --status-renamed: #58a6ff; + --status-untracked: #91889b; + + --diff-added-bg: rgba(63, 185, 80, 0.08); + --diff-removed-bg: rgba(248, 81, 73, 0.08); + --diff-changed-bg: rgba(255, 255, 255, 0.04); + --diff-range-border: #524d58; + --diff-comment-highlight: rgba(88, 166, 255, 0.5); + + --search-match-bg: rgba(250, 200, 50, 0.35); + --search-current-match-bg: rgba(255, 150, 50, 0.5); + + --annotation-overlay-bg: rgba(0, 0, 0, 0.5); + --annotation-border: rgba(255, 255, 255, 0.1); + + --ui-accent: #3fb950; + --ui-accent-hover: #2ea043; + --ui-success: #3fb950; + --ui-danger: #f85149; + --ui-danger-bg: rgba(248, 81, 73, 0.1); + --ui-selection: rgba(255, 255, 255, 0.08); + + --scrollbar-thumb: #47424d; + --scrollbar-thumb-hover: #5d5962; + --scrollbar-thumb-transparent: rgba(255, 255, 255, 0.15); + --scrollbar-thumb-hover-transparent: rgba(255, 255, 255, 0.25); + + --shadow-overlay: rgba(0, 0, 0, 0.6); + --shadow-elevated: 0 8px 24px rgba(0, 0, 0, 0.4); + + --size-base: 13px; + --size-xs: calc(var(--size-base) * 0.846); + --size-sm: calc(var(--size-base) * 0.923); + --size-md: var(--size-base); + --size-lg: calc(var(--size-base) * 1.077); + --size-xl: calc(var(--size-base) * 1.231); + + --accent-primary: #58a6ff; + + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + font-size: var(--size-md); + line-height: 1.5; + font-weight: 400; + + color: var(--text-primary); + background-color: var(--bg-primary); + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } body { margin: 0; padding: 0; + min-height: 100vh; +} + +#app { + min-height: 100vh; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-thumb-hover); } diff --git a/apps/staged/src/lib/FolderPickerModal.svelte b/apps/staged/src/lib/FolderPickerModal.svelte new file mode 100644 index 00000000..3d061f86 --- /dev/null +++ b/apps/staged/src/lib/FolderPickerModal.svelte @@ -0,0 +1,734 @@ + + + + + + + + + diff --git a/apps/staged/src/lib/GitTreeAnimation.svelte b/apps/staged/src/lib/GitTreeAnimation.svelte new file mode 100644 index 00000000..0f44fa90 --- /dev/null +++ b/apps/staged/src/lib/GitTreeAnimation.svelte @@ -0,0 +1,554 @@ + + + +
+ +
+ + diff --git a/apps/staged/src/lib/MarkIcon.svelte b/apps/staged/src/lib/MarkIcon.svelte new file mode 100644 index 00000000..71c61269 --- /dev/null +++ b/apps/staged/src/lib/MarkIcon.svelte @@ -0,0 +1,45 @@ + + + + + + diff --git a/apps/staged/src/lib/ThemePicker.svelte b/apps/staged/src/lib/ThemePicker.svelte new file mode 100644 index 00000000..6f7f2529 --- /dev/null +++ b/apps/staged/src/lib/ThemePicker.svelte @@ -0,0 +1,247 @@ + + + + + +
+
+ + +
+ +
+ {#each filteredThemes as theme, i (theme.name)} + + {:else} +
No themes match "{searchQuery}"
+ {/each} +
+
+ + diff --git a/apps/staged/src/lib/commands.ts b/apps/staged/src/lib/commands.ts new file mode 100644 index 00000000..c8252a19 --- /dev/null +++ b/apps/staged/src/lib/commands.ts @@ -0,0 +1,166 @@ +/** + * Typed invoke wrappers for Staged's Tauri commands. + */ + +import { invoke } from '@tauri-apps/api/core'; +import type { FileDiffSummary, FileDiff, File } from '@builderbot/diff-viewer/types'; + +// ============================================================================= +// Types (matching Rust backend) +// ============================================================================= + +export interface RepoInfo { + path: string; + branch: string; + defaultBranch: string; + commitsAhead: number; +} + +export interface CommitInfo { + sha: string; + shortSha: string; + message: string; + author: string; + timestamp: number; +} + +export interface DiffFilesResponse { + files: FileDiffSummary[]; +} + +export interface LaunchArgs { + repoPath: string; + mode: string | null; + commit: string | null; +} + +export interface DirEntry { + name: string; + path: string; + isDir: boolean; + isRepo: boolean; +} + +export interface RecentRepo { + name: string; + path: string; +} + +/** Matches the git-diff crate's GitRef enum (tagged union). */ +export type GitRef = + | { type: 'WorkingTree' } + | { type: 'Index' } + | { type: 'Rev'; value: string } + | { type: 'MergeBase' } + | { type: 'MergeBaseOf'; value: [string, string] }; + +export interface DiffSpec { + base: GitRef; + head: GitRef; +} + +// ============================================================================= +// Diff spec builders +// ============================================================================= + +/** All uncommitted changes: HEAD -> working tree */ +export function specUncommitted(): DiffSpec { + return { + base: { type: 'Rev', value: 'HEAD' }, + head: { type: 'WorkingTree' }, + }; +} + +/** Staged changes only: HEAD -> index */ +export function specStaged(): DiffSpec { + return { + base: { type: 'Rev', value: 'HEAD' }, + head: { type: 'Index' }, + }; +} + +/** Full branch diff: merge-base -> HEAD */ +export function specBranch(): DiffSpec { + return { + base: { type: 'MergeBase' }, + head: { type: 'Rev', value: 'HEAD' }, + }; +} + +/** Single commit: parent -> commit */ +export function specCommit(sha: string): DiffSpec { + return { + base: { type: 'Rev', value: `${sha}~1` }, + head: { type: 'Rev', value: sha }, + }; +} + +/** Range from a commit to HEAD */ +export function specRange(fromSha: string): DiffSpec { + return { + base: { type: 'Rev', value: fromSha }, + head: { type: 'Rev', value: 'HEAD' }, + }; +} + +// ============================================================================= +// Commands +// ============================================================================= + +export function getRepoInfo(): Promise { + return invoke('get_repo_info'); +} + +export function listRecentCommits(count?: number): Promise { + return invoke('list_recent_commits', { count: count ?? null }); +} + +export function listDiffFiles(spec: DiffSpec): Promise { + return invoke('list_diff_files', { spec }); +} + +export function getFileDiff(spec: DiffSpec, path: string): Promise { + return invoke('get_file_diff', { spec, path }); +} + +export function getFileAtRef(refName: string, path: string): Promise { + return invoke('get_file_at_ref', { refName, path }); +} + +export function getLaunchArgs(): Promise { + return invoke('get_launch_args'); +} + +export function setRepoPath(path: string): Promise { + return invoke('set_repo_path', { path }); +} + +// ============================================================================= +// Directory browsing +// ============================================================================= + +export function listDirectory(path: string): Promise { + return invoke('list_directory', { path }); +} + +export function searchDirectories( + path: string, + query: string, + maxDepth?: number, + limit?: number +): Promise { + return invoke('search_directories', { + path, + query, + maxDepth: maxDepth ?? 3, + limit: limit ?? 20, + }); +} + +export function getHomeDir(): Promise { + return invoke('get_home_dir'); +} + +export function findRecentRepos(hoursAgo?: number, limit?: number): Promise { + return invoke('find_recent_repos', { hoursAgo, limit }); +} diff --git a/apps/staged/src/lib/preferences.svelte.ts b/apps/staged/src/lib/preferences.svelte.ts new file mode 100644 index 00000000..c069a10c --- /dev/null +++ b/apps/staged/src/lib/preferences.svelte.ts @@ -0,0 +1,118 @@ +/** + * User Preferences Store for Staged + * + * Manages persistent user preferences (Tauri store-backed). + * Handles syntax theme selection with adaptive UI theming. + */ + +import { + SYNTAX_THEMES, + setSyntaxTheme, + getTheme, + isLightTheme, + initHighlighter, + type SyntaxThemeName, +} from '@builderbot/diff-viewer/utils'; +import { load, type Store } from '@tauri-apps/plugin-store'; +import { createAdaptiveTheme, themeToVarMap } from '../../../mark/src/lib/theme'; + +// Re-export for convenience +export { isLightTheme }; + +// ============================================================================= +// Constants +// ============================================================================= + +const SYNTAX_THEME_STORE_KEY = 'syntax-theme'; +const DEFAULT_SYNTAX_THEME: SyntaxThemeName = 'laserwave'; + +// ============================================================================= +// Store +// ============================================================================= + +let store: Store | null = null; + +async function initStore(): Promise { + if (store) return; + store = await load('preferences.json', { + defaults: {}, + autoSave: true, + overrideDefaults: true, + }); +} + +async function getStoreValue(key: string): Promise { + if (!store) return undefined; + return store.get(key); +} + +async function setStoreValue(key: string, value: T): Promise { + if (!store) return; + await store.set(key, value); +} + +// ============================================================================= +// Reactive State +// ============================================================================= + +export interface ThemeEntry { + name: string; +} + +export const preferences = $state({ + syntaxTheme: DEFAULT_SYNTAX_THEME as string, + loaded: false, +}); + +// ============================================================================= +// CSS Application +// ============================================================================= + +function applyAdaptiveTheme() { + const themeInfo = getTheme(); + if (themeInfo) { + const adaptiveTheme = createAdaptiveTheme(themeInfo.bg, themeInfo.fg, themeInfo.comment, { + added: themeInfo.added, + deleted: themeInfo.deleted, + modified: themeInfo.modified, + }); + const varMap = themeToVarMap(adaptiveTheme); + const style = document.documentElement.style; + for (const [prop, value] of Object.entries(varMap)) { + style.setProperty(prop, value); + } + } +} + +// ============================================================================= +// Initialization +// ============================================================================= + +export async function initPreferences(): Promise { + await initStore(); + + const savedTheme = await getStoreValue(SYNTAX_THEME_STORE_KEY); + if (savedTheme && SYNTAX_THEMES.includes(savedTheme as SyntaxThemeName)) { + preferences.syntaxTheme = savedTheme; + } + + await initHighlighter(preferences.syntaxTheme as SyntaxThemeName); + applyAdaptiveTheme(); + + preferences.loaded = true; +} + +// ============================================================================= +// Theme Actions +// ============================================================================= + +export function getAvailableSyntaxThemes(): ThemeEntry[] { + return SYNTAX_THEMES.map((name) => ({ name })); +} + +export async function selectSyntaxTheme(name: string): Promise { + await setSyntaxTheme(name as SyntaxThemeName); + preferences.syntaxTheme = name; + await setStoreValue(SYNTAX_THEME_STORE_KEY, name); + applyAdaptiveTheme(); +} diff --git a/apps/staged/src/types/sanitize-html.d.ts b/apps/staged/src/types/sanitize-html.d.ts new file mode 100644 index 00000000..93270031 --- /dev/null +++ b/apps/staged/src/types/sanitize-html.d.ts @@ -0,0 +1,20 @@ +declare module 'sanitize-html' { + type AllowedAttributes = Record; + + interface SanitizeHtmlOptions { + allowedTags?: string[]; + allowedAttributes?: AllowedAttributes; + allowedSchemes?: string[]; + } + + interface SanitizeHtmlFn { + (dirty: string, options?: SanitizeHtmlOptions): string; + defaults: { + allowedTags: string[]; + allowedAttributes: AllowedAttributes; + }; + } + + const sanitizeHtml: SanitizeHtmlFn; + export default sanitizeHtml; +} diff --git a/apps/staged/tsconfig.app.json b/apps/staged/tsconfig.app.json index 90ed3c78..6e2c2f19 100644 --- a/apps/staged/tsconfig.app.json +++ b/apps/staged/tsconfig.app.json @@ -9,7 +9,8 @@ "noEmit": true, "allowJs": true, "checkJs": true, - "moduleDetection": "force" + "moduleDetection": "force", + "skipLibCheck": true }, "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] } diff --git a/crates/git-diff/src/diff.rs b/crates/git-diff/src/diff.rs index 5bf335b1..5f192ad0 100644 --- a/crates/git-diff/src/diff.rs +++ b/crates/git-diff/src/diff.rs @@ -46,6 +46,10 @@ pub fn get_unified_diff(repo: &Path, spec: &DiffSpec, path: &Path) -> Result { + // Diff from commit to staging area + cli::run(repo, &["diff", "--cached", base.as_str(), "--", path_str]) + } (GitRef::Rev(base), GitRef::Rev(head)) => { // Diff between two commits cli::run( @@ -53,8 +57,8 @@ pub fn get_unified_diff(repo: &Path, spec: &DiffSpec, path: &Path) -> Result Err(GitError::CommandFailed( - "Cannot use working tree as base".to_string(), + (GitRef::WorkingTree, _) | (GitRef::Index, _) => Err(GitError::CommandFailed( + "Cannot use working tree or index as base".to_string(), )), (GitRef::MergeBase | GitRef::MergeBaseOf(_), _) | (_, GitRef::MergeBase | GitRef::MergeBaseOf(_)) => { @@ -92,14 +96,20 @@ pub fn list_diff_files(repo: &Path, spec: &DiffSpec) -> Result { + // Staged changes: diff between a rev and the index + let args = ["diff", "--cached", "--name-status", "-z", base.as_str()]; + let output = cli::run(repo, &args)?; + parse_name_status(&output) + } (GitRef::Rev(base), GitRef::Rev(head)) => { // Commit range - use git diff let args = ["diff", "--name-status", "-z", base.as_str(), head.as_str()]; let output = cli::run(repo, &args)?; parse_name_status(&output) } - (GitRef::WorkingTree, _) => Err(GitError::CommandFailed( - "Cannot use working tree as base".to_string(), + (GitRef::WorkingTree, _) | (GitRef::Index, _) => Err(GitError::CommandFailed( + "Cannot use working tree or index as base".to_string(), )), (GitRef::MergeBase | GitRef::MergeBaseOf(_), _) | (_, GitRef::MergeBase | GitRef::MergeBaseOf(_)) => { @@ -322,13 +332,20 @@ pub fn get_file_diff(repo_path: &Path, spec: &DiffSpec, path: &Path) -> Result Result( git_ref: &GitRef, ) -> Result>, GitError> { match git_ref { - GitRef::WorkingTree => Ok(None), + GitRef::WorkingTree | GitRef::Index => Ok(None), GitRef::Rev(rev) => { let obj = repo .revparse_single(rev) @@ -410,6 +428,29 @@ fn load_file_from_tree( })) } +/// Load file content from the git index (staging area) +fn load_file_from_index(repo: &Repository, path: &Path) -> Result, GitError> { + let index = repo + .index() + .map_err(|e| GitError::CommandFailed(format!("Cannot read index: {e}")))?; + + let entry = match index.get_path(path, 0) { + Some(e) => e, + None => return Ok(None), + }; + + let blob = repo + .find_blob(entry.id) + .map_err(|e| GitError::CommandFailed(format!("Cannot load blob from index: {e}")))?; + + let content = bytes_to_content(blob.content()); + + Ok(Some(File { + path: path.to_string_lossy().to_string(), + content, + })) +} + /// Load file content from the working directory fn load_file_from_workdir(repo: &Repository, path: &Path) -> Result, GitError> { let workdir = repo @@ -455,13 +496,17 @@ fn get_hunks_libgit2( base_tree: Option<&git2::Tree>, head_tree: Option<&git2::Tree>, is_working_tree: bool, + is_index: bool, path: &Path, ) -> Result, GitError> { let mut opts = DiffOptions::new(); opts.context_lines(0); // No context, just the changes opts.pathspec(path); - let diff = if is_working_tree { + let diff = if is_index { + // Staged changes: tree → index + repo.diff_tree_to_index(base_tree, None, Some(&mut opts)) + } else if is_working_tree { repo.diff_tree_to_workdir_with_index(base_tree, Some(&mut opts)) } else { repo.diff_tree_to_tree(base_tree, head_tree, Some(&mut opts)) diff --git a/crates/git-diff/src/types.rs b/crates/git-diff/src/types.rs index 9de54726..bcff90ae 100644 --- a/crates/git-diff/src/types.rs +++ b/crates/git-diff/src/types.rs @@ -10,6 +10,8 @@ pub const WORKDIR: &str = "WORKDIR"; pub enum GitRef { /// The working tree (uncommitted changes) WorkingTree, + /// The staging area / index (what has been `git add`'d) + Index, /// Anything that resolves to a commit: SHA, branch, tag, origin/main, HEAD~3, etc. Rev(String), /// Merge-base between the default branch and HEAD. @@ -27,6 +29,7 @@ impl GitRef { pub fn as_git_arg(&self) -> Option<&str> { match self { GitRef::WorkingTree => None, + GitRef::Index => None, GitRef::Rev(s) => Some(s), GitRef::MergeBase => panic!("MergeBase must be resolved before use"), GitRef::MergeBaseOf(_) => panic!("MergeBaseOf must be resolved before use"), @@ -37,6 +40,7 @@ impl GitRef { pub fn display(&self) -> &str { match self { GitRef::WorkingTree => "@", + GitRef::Index => "index", GitRef::Rev(s) => s, GitRef::MergeBase => "merge-base", GitRef::MergeBaseOf(_) => "merge-base", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7eed3cdf..f2b4ae94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,9 +71,30 @@ importers: apps/staged: dependencies: + '@builderbot/diff-viewer': + specifier: workspace:* + version: link:../../packages/diff-viewer '@tauri-apps/api': specifier: ^2.10.0 version: 2.10.1 + '@tauri-apps/plugin-dialog': + specifier: ^2.2.0 + version: 2.6.0 + '@tauri-apps/plugin-store': + specifier: ^2.2.0 + version: 2.4.2 + lucide-svelte: + specifier: ^0.575.0 + version: 0.575.0(svelte@5.53.2) + marked: + specifier: ^17.0.1 + version: 17.0.3 + sanitize-html: + specifier: ^2.17.0 + version: 2.17.1 + shiki: + specifier: ^3.20.0 + version: 3.22.0 devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^6.2.1 @@ -87,6 +108,9 @@ importers: '@types/node': specifier: ^24.10.1 version: 24.10.13 + '@types/sanitize-html': + specifier: ^2.13.0 + version: 2.16.0 prettier: specifier: ^3.7.4 version: 3.8.1 @@ -384,7 +408,6 @@ packages: resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} @@ -563,6 +586,9 @@ packages: '@tauri-apps/plugin-clipboard-manager@2.3.2': resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==} + '@tauri-apps/plugin-dialog@2.6.0': + resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==} + '@tauri-apps/plugin-store@2.4.2': resolution: {integrity: sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A==} @@ -581,6 +607,9 @@ packages: '@types/node@24.10.13': resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==} + '@types/sanitize-html@2.16.0': + resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -1216,6 +1245,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.10.1 + '@tauri-apps/plugin-dialog@2.6.0': + dependencies: + '@tauri-apps/api': 2.10.1 + '@tauri-apps/plugin-store@2.4.2': dependencies: '@tauri-apps/api': 2.10.1 @@ -1236,6 +1269,10 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/sanitize-html@2.16.0': + dependencies: + htmlparser2: 8.0.2 + '@types/trusted-types@2.0.7': {} '@types/unist@3.0.3': {}