Merge branch 'master' into ian/gb-220-ai-chat-loading-state-ux

This commit is contained in:
Ian Donahue 2023-04-25 14:20:50 +02:00 committed by GitHub
commit 9ba9ec3be4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 1787 additions and 1527 deletions

1
.gitignore vendored
View File

@ -28,6 +28,7 @@ dist-ssr
/package
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# gitbutler
.git/gb-*

View File

@ -37,6 +37,7 @@
"@lezer/javascript": "^1.4.1",
"@lezer/lr": "^1.3.3",
"@replit/codemirror-lang-svelte": "^6.0.0",
"@sentry/sveltekit": "^7.49.0",
"@square/svelte-store": "^1.0.14",
"@storybook/addon-essentials": "next",
"@storybook/addon-interactions": "next",

File diff suppressed because it is too large Load Diff

View File

@ -178,7 +178,7 @@ trap "rm -rf '$TMP_DIR'" exit
jq '.package.version="'"$VERSION"'"' "$PWD/../src-tauri/tauri.conf.release.json" >"$TMP_DIR/tauri.conf.json"
# build the app with release config
tauri build --config "$TMP_DIR/tauri.conf.json"
SENTRY_RELEASE="$VERSION" tauri build --config "$TMP_DIR/tauri.conf.json"
BUNDLE_DIR="$PWD/../src-tauri/target/release/bundle"
MACOS_DMG="$(find "$BUNDLE_DIR/dmg" -depth 1 -type f -name "*.dmg")"

327
src-tauri/Cargo.lock generated
View File

@ -66,6 +66,9 @@ name = "anyhow"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
dependencies = [
"backtrace",
]
[[package]]
name = "arc-swap"
@ -528,16 +531,16 @@ dependencies = [
[[package]]
name = "crash-handler"
version = "0.3.3"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b333adce79bc5ee4f421211a160514ed5a3f4dc9cd4202021d70ebdd7657e50"
checksum = "37571ca6b1166c54bdde2b482eea4c935207d5d82889382b327ad6fcf88ce656"
dependencies = [
"cfg-if",
"crash-context",
"libc",
"mach2",
"parking_lot",
"windows-sys 0.36.1",
"windows-sys 0.42.0",
]
[[package]]
@ -985,6 +988,18 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "findshlibs"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64"
dependencies = [
"cc",
"lazy_static",
"libc",
"winapi",
]
[[package]]
name = "flate2"
version = "1.0.25"
@ -1333,8 +1348,10 @@ dependencies = [
"portable-pty",
"reqwest",
"scopeguard",
"sentry",
"sentry-tauri",
"sentry 0.31.0",
"sentry-anyhow",
"sentry-debug-images 0.31.0",
"sentry-rust-minidump",
"serde",
"serde-jsonlines",
"serde_json",
@ -2226,9 +2243,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minidump-common"
version = "0.12.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a134e10507b4352836be67e13cfa06e7881df5b4e091999e75c05eb449dc6985"
checksum = "97dbaf56dfe28d07e1fecffce410976774dcac1f0d7f6d797b437468e989e687"
dependencies = [
"bitflags 1.3.2",
"debugid",
@ -2242,9 +2259,9 @@ dependencies = [
[[package]]
name = "minidump-writer"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e663ad99c0ad5f62dec920482cae3dc84d4138395efbac39a3c1ab817fe9d59"
checksum = "8c8c8fcc3823f1f4eec257a24990ab4c6a20ef9bcbc515400d0eb4a99620b943"
dependencies = [
"byteorder",
"cfg-if",
@ -2255,18 +2272,18 @@ dependencies = [
"memmap2",
"memoffset 0.6.5",
"minidump-common",
"nix 0.24.3",
"nix",
"scroll",
"tempfile",
"thiserror",
"windows-sys 0.36.1",
"windows-sys 0.42.0",
]
[[package]]
name = "minidumper"
version = "0.5.1"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bc0932e7cf3760d8ea4784bbe7ec45cc1970c08d1f6e7d108d8da1890d26586"
checksum = "a8e7577424f7abeacef16980ed8e200c84e74aaabb799c3ee30159ea877534fb"
dependencies = [
"cfg-if",
"crash-context",
@ -2278,7 +2295,19 @@ dependencies = [
"scroll",
"thiserror",
"uds",
"windows-sys 0.36.1",
"windows-sys 0.42.0",
]
[[package]]
name = "minidumper-child"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f6a7c1a63d65dd21fa335135e5a614a7adb03560b2feb9fcb70eef34439bc8"
dependencies = [
"crash-handler",
"minidumper",
"thiserror",
"uuid 1.3.1",
]
[[package]]
@ -2373,17 +2402,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nix"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
dependencies = [
"bitflags 1.3.2",
"cfg-if",
"libc",
]
[[package]]
name = "nix"
version = "0.25.1"
@ -2638,6 +2656,17 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "os_info"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e"
dependencies = [
"log",
"serde",
"winapi",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -2897,7 +2926,7 @@ dependencies = [
"lazy_static",
"libc",
"log",
"nix 0.25.1",
"nix",
"serde",
"serde_derive",
"serial",
@ -3436,99 +3465,205 @@ dependencies = [
[[package]]
name = "sentry"
version = "0.27.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73642819e7fa63eb264abc818a2f65ac8764afbe4870b5ee25bcecc491be0d4c"
checksum = "b5ce6d3512e2617c209ec1e86b0ca2fea06454cd34653c91092bf0f3ec41f8e3"
dependencies = [
"httpdate",
"native-tls",
"reqwest",
"sentry-backtrace",
"sentry-contexts",
"sentry-core",
"sentry-panic",
"sentry-backtrace 0.30.0",
"sentry-contexts 0.30.0",
"sentry-core 0.30.0",
"sentry-debug-images 0.30.0",
"sentry-panic 0.30.0",
"tokio",
"ureq",
]
[[package]]
name = "sentry"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c3d7f8bf7373e75222452fcdd9347d857452a92d0eec738f941bc4656c5b5df"
dependencies = [
"httpdate",
"native-tls",
"reqwest",
"sentry-anyhow",
"sentry-backtrace 0.31.0",
"sentry-contexts 0.31.0",
"sentry-core 0.31.0",
"sentry-debug-images 0.31.0",
"sentry-panic 0.31.0",
"tokio",
"ureq",
]
[[package]]
name = "sentry-anyhow"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ef7f47c57a1146d553b4976f20e8bba370195a88858bdf6945a63c529549236"
dependencies = [
"anyhow",
"sentry-backtrace 0.31.0",
"sentry-core 0.31.0",
]
[[package]]
name = "sentry-backtrace"
version = "0.27.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49bafa55eefc6dbc04c7dac91e8c8ab9e89e9414f3193c105cabd991bbc75134"
checksum = "0e7fe408d4d1f8de188a9309916e02e129cbe51ca19e55badea5a64899399b1a"
dependencies = [
"backtrace",
"once_cell",
"regex",
"sentry-core",
"sentry-core 0.30.0",
]
[[package]]
name = "sentry-backtrace"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03b7cdefbdca51f1146f0f24a3cb4ecb6428951f030ff5c720cfb5c60bd174c0"
dependencies = [
"backtrace",
"once_cell",
"regex",
"sentry-core 0.31.0",
]
[[package]]
name = "sentry-contexts"
version = "0.27.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c63317c4051889e73f0b00ce4024cae3e6a225f2e18a27d2c1522eb9ce2743da"
checksum = "5695096a059a89973ec541062d331ff4c9aeef9c2951416c894f0fff76340e7d"
dependencies = [
"hostname",
"libc",
"os_info",
"rustc_version",
"sentry-core",
"sentry-core 0.30.0",
"uname",
]
[[package]]
name = "sentry-contexts"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af4cb29066e0e8df0cc3111211eb93543ccb09e1ccbe71de6d88b4bb459a2b1"
dependencies = [
"hostname",
"libc",
"os_info",
"rustc_version",
"sentry-core 0.31.0",
"uname",
]
[[package]]
name = "sentry-core"
version = "0.27.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a4591a2d128af73b1b819ab95f143bc6a2fbe48cd23a4c45e1ee32177e66ae6"
checksum = "5b22828bfd118a7b660cf7a155002a494755c0424cebb7061e4743ecde9c7dbc"
dependencies = [
"once_cell",
"rand 0.8.5",
"sentry-types",
"sentry-types 0.30.0",
"serde",
"serde_json",
]
[[package]]
name = "sentry-panic"
version = "0.27.0"
name = "sentry-core"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "696c74c5882d5a0d5b4a31d0ff3989b04da49be7983b7f52a52c667da5b480bf"
checksum = "5e781b55761e47a60d1ff326ae8059de22b0e6b0cee68eab1c5912e4fb199a76"
dependencies = [
"sentry-backtrace",
"sentry-core",
"once_cell",
"rand 0.8.5",
"sentry-types 0.31.0",
"serde",
"serde_json",
]
[[package]]
name = "sentry-debug-images"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a9164d44a2929b1b7670afd7e87552514b70d3ae672ca52884639373d912a3d"
dependencies = [
"findshlibs",
"once_cell",
"sentry-core 0.30.0",
]
[[package]]
name = "sentry-debug-images"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e758030b31ee2cd97424a980dfa34a12dcd8477424861cf81ae3aa1f9f616a8c"
dependencies = [
"findshlibs",
"once_cell",
"sentry-core 0.31.0",
]
[[package]]
name = "sentry-panic"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4ced2a7a8c14899d58eec402d946f69d5ed26a3fc363a7e8b1e5cb88473a01"
dependencies = [
"sentry-backtrace 0.30.0",
"sentry-core 0.30.0",
]
[[package]]
name = "sentry-panic"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e0b877981990d9e84ae6916df61993d188fdf76afb59521f0aeaf9b8e6d26d0"
dependencies = [
"sentry-backtrace 0.31.0",
"sentry-core 0.31.0",
]
[[package]]
name = "sentry-rust-minidump"
version = "0.1.3"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d986b33fedaa7638225e07ac1cc0217894ee2958798a09e2d61b2fe5f486bc89"
checksum = "a489204e5cb676310d7ef1506eefb76d006a82f27b648f5477ea74733467d5a1"
dependencies = [
"crash-handler",
"dirs-next",
"minidumper",
"sentry",
"thiserror",
"uuid 1.3.1",
]
[[package]]
name = "sentry-tauri"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13a2132c1f33f471f3842001c1aee74df0b94c10628833da798275285bcf0f9"
dependencies = [
"sentry",
"sentry-rust-minidump",
"serde",
"tauri",
"minidumper-child",
"sentry 0.30.0",
"thiserror",
]
[[package]]
name = "sentry-types"
version = "0.27.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "823923ae5f54a729159d720aa12181673044ee5c79cbda3be09e56f885e5468f"
checksum = "360ee3270f7a4a1eee6c667f7d38360b995431598a73b740dfe420da548d9cc9"
dependencies = [
"debugid",
"getrandom 0.2.9",
"hex",
"serde",
"serde_json",
"thiserror",
"time",
"url",
"uuid 1.3.1",
]
[[package]]
name = "sentry-types"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d642a04657cc77d8de52ae7c6d93a15cb02284eb219344a89c1e2b26bbaf578c"
dependencies = [
"debugid",
"getrandom 0.2.9",
@ -4745,6 +4880,19 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "ureq"
version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d"
dependencies = [
"base64 0.13.1",
"log",
"native-tls",
"once_cell",
"url",
]
[[package]]
name = "url"
version = "2.3.1"
@ -5123,19 +5271,6 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278"
[[package]]
name = "windows-sys"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc 0.36.1",
"windows_i686_gnu 0.36.1",
"windows_i686_msvc 0.36.1",
"windows_x86_64_gnu 0.36.1",
"windows_x86_64_msvc 0.36.1",
]
[[package]]
name = "windows-sys"
version = "0.42.0"
@ -5217,12 +5352,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
version = "0.37.0"
@ -5247,12 +5376,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
version = "0.37.0"
@ -5277,12 +5400,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
version = "0.37.0"
@ -5307,12 +5424,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.37.0"
@ -5349,12 +5460,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
version = "0.37.0"

View File

@ -26,8 +26,7 @@ uuid = "1.3.0"
git2 = { version = "0.16.1", features = ["vendored-openssl", "vendored-libgit2"] }
filetime = "0.2.19"
sha2 = "0.10.6"
sentry-tauri = "0.1.0"
sentry = "0.27"
sentry = {version = "0.31.0", features = ["backtrace", "contexts", "panic", "transport", "anyhow", "debug-images", "reqwest", "native-tls" ] }
walkdir = "2.3.2"
anyhow = "1.0.69"
tempfile = "3.3.0"
@ -48,6 +47,9 @@ timed = "0.2.1"
serde-jsonlines = "0.4.0"
crossbeam-channel = "0.5.8"
scopeguard = "1.1.0"
sentry-anyhow = "0.31.0"
sentry-rust-minidump = "0.5.1"
sentry-debug-images = "0.31.0"
[features]
# by default Tauri runs in production mode

View File

@ -493,7 +493,6 @@ fn build_wd_tree(
abs_path.display(),
e
);
println!("failed to read file {}: {:#}", abs_path.display(), e);
continue;
}
};

View File

@ -2,7 +2,6 @@ use std::{collections::HashMap, env};
use anyhow::{Context, Result};
use serde::Serialize;
use tauri::regex::Regex;
use walkdir::WalkDir;
use crate::{git::activity, projects};
@ -211,25 +210,23 @@ impl<'repository> Repository<'repository> {
.workdir()
.with_context(|| "failed to get working directory")?;
let pattern = Regex::new(pattern).with_context(|| "regex parse error");
match pattern {
Ok(pattern) => {
let mut files = vec![];
for entry in WalkDir::new(workdir)
let pattern = pattern.to_lowercase();
let mut files = vec![];
for entry in WalkDir::new(workdir)
.into_iter()
.filter_entry(|e| {
.filter_entry(|entry| {
// need to remove workdir so we're not matching it
let match_string = e
let relative_path = entry
.path()
.strip_prefix::<&std::path::Path>(workdir.as_ref())
.strip_prefix(workdir)
.unwrap()
.to_str()
.unwrap();
// this is to make it faster, so we dont have to traverse every directory if it is ignored by git
e.path().to_str() == workdir.to_str() // but we need to traverse the first one
|| ((e.file_type().is_dir() // traverse all directories if they are not ignored by git
|| pattern.is_match(match_string)) // but only pass on files that match the regex
&& !self.git_repository.is_path_ignored(&e.path()).unwrap_or(true))
entry.path().to_str() == workdir.to_str() // but we need to traverse the first one
|| ((entry.file_type().is_dir() // traverse all directories if they are not ignored by git
|| relative_path.to_lowercase().contains(&pattern)) // but only pass on files that match the regex
&& !self.git_repository.is_path_ignored(&entry.path()).unwrap_or(true))
})
.filter_map(Result::ok)
{
@ -248,13 +245,8 @@ impl<'repository> Repository<'repository> {
files.push(path);
}
}
files.sort();
return Ok(files);
}
Err(e) => {
return Err(e);
}
}
files.sort();
return Ok(files);
}
pub fn git_branches(&self) -> Result<Vec<String>> {

View File

@ -91,7 +91,6 @@ pub fn get_delta_operations(initial_text: &str, final_text: &str) -> Vec<Operati
let mut offset = 0;
for change in changeset.iter_all_changes() {
println!("{:?}", change);
match change.tag() {
ChangeTag::Delete => {
deltas.push(Operation::Delete((

View File

@ -58,6 +58,7 @@ impl From<projects::CreateError> for Error {
impl From<anyhow::Error> for Error {
fn from(e: anyhow::Error) -> Self {
sentry_anyhow::capture_anyhow(&e);
log::error!("{:#}", e);
Error::Unknown
}
@ -65,14 +66,6 @@ impl From<anyhow::Error> for Error {
const IS_DEV: bool = cfg!(debug_assertions);
fn app_title() -> String {
if IS_DEV {
"GitButler (dev)".to_string()
} else {
"GitButler".to_string()
}
}
fn build_asset_url(path: &str) -> String {
format!("asset://localhost/{}", urlencoding::encode(path))
}
@ -410,15 +403,27 @@ async fn git_commit(
}
fn main() {
let tauri_context = generate_context!();
let _guard = sentry::init(("https://9d407634d26b4d30b6a42d57a136d255@o4504644069687296.ingest.sentry.io/4504649768108032", sentry::ClientOptions {
release: Some(tauri_context.package_info().version.to_string().into()),
attach_stacktrace: true,
default_integrations: true,
..Default::default()
}));
let app_title = tauri_context.package_info().name.clone();
let quit = tauri::CustomMenuItem::new("quit".to_string(), "Quit");
let hide = tauri::CustomMenuItem::new("toggle".to_string(), format!("Hide {}", app_title()));
let hide = tauri::CustomMenuItem::new("toggle".to_string(), format!("Hide {}", app_title));
let tray_menu = tauri::SystemTrayMenu::new().add_item(hide).add_item(quit);
let tray = tauri::SystemTray::new().with_menu(tray_menu);
let tauri_app_builder = tauri::Builder::default()
tauri::Builder::default()
.system_tray(tray)
.on_system_tray_event(|app_handle, event| match event {
tauri::SystemTrayEvent::MenuItemClick { id, .. } => {
let app_title = app_handle.package_info().name.clone();
let item_handle = app_handle.tray_handle().get_item(&id);
match id.as_str() {
"quit" => {
@ -429,20 +434,20 @@ fn main() {
if window.is_visible().unwrap() {
window.hide().unwrap();
item_handle
.set_title(format!("Show {}", app_title()))
.set_title(format!("Show {}", app_title))
.unwrap();
} else {
window.show().unwrap();
window.set_focus().unwrap();
item_handle
.set_title(format!("Hide {}", app_title()))
.set_title(format!("Hide {}", app_title))
.unwrap();
}
}
None => {
create_window(&app_handle).expect("Failed to create window");
item_handle
.set_title(format!("Hide {}", app_title()))
.set_title(format!("Hide {}", app_title))
.unwrap();
}
},
@ -455,12 +460,13 @@ fn main() {
tauri::WindowEvent::CloseRequested { api, .. } => {
api.prevent_close();
let window = event.window();
let app_handle = window.app_handle();
let app_title = app_handle.package_info().name.clone();
window
.app_handle()
app_handle
.tray_handle()
.get_item("toggle")
.set_title(format!("Show {}", app_title()))
.set_title(format!("Show {}", app_title))
.expect("Failed to set tray item title");
window.hide().expect("Failed to hide window");
@ -544,37 +550,16 @@ fn main() {
git_stage,
git_unstage,
git_wd_diff,
]);
let tauri_context = generate_context!();
let app_version = tauri_context.package_info().version.to_string();
sentry_tauri::init(
app_version.clone(),
|_| {
sentry::init((
"https://9d407634d26b4d30b6a42d57a136d255@o4504644069687296.ingest.sentry.io/4504649768108032",
sentry::ClientOptions {
release: Some(std::borrow::Cow::from(app_version)),
..Default::default()
},
))
},
|sentry_plugin| {
let tauri_app = tauri_app_builder
.plugin(sentry_plugin)
.build(tauri_context)
.expect("Failed to build tauri app");
tauri_app.run(|app_handle, event| match event {
tauri::RunEvent::ExitRequested { api, .. } => {
hide_window(&app_handle).expect("Failed to hide window");
api.prevent_exit();
}
_ => {}
});
},
);
])
.build(tauri_context)
.expect("Failed to build tauri app")
.run(|app_handle, event| match event {
tauri::RunEvent::ExitRequested { api, .. } => {
hide_window(&app_handle).expect("Failed to hide window");
api.prevent_exit();
}
_ => {}
});
}
fn init(app_handle: tauri::AppHandle) -> Result<()> {
@ -616,9 +601,10 @@ fn get_window(handle: &tauri::AppHandle) -> Option<tauri::Window> {
#[cfg(not(target_os = "macos"))]
fn create_window(handle: &tauri::AppHandle) -> tauri::Result<tauri::Window> {
log::info!("Creating window");
let app_title = handle.package_info().name.clone();
tauri::WindowBuilder::new(handle, "main", tauri::WindowUrl::App("index.html".into()))
.resizable(true)
.title(app_title())
.title(app_title)
.theme(Some(tauri::Theme::Dark))
.min_inner_size(600.0, 300.0)
.inner_size(800.0, 600.0)
@ -630,7 +616,7 @@ fn create_window(handle: &tauri::AppHandle) -> tauri::Result<tauri::Window> {
log::info!("Creating window");
tauri::WindowBuilder::new(handle, "main", tauri::WindowUrl::App("index.html".into()))
.resizable(true)
.title(app_title())
.title(handle.package_info().name.clone())
.theme(Some(tauri::Theme::Dark))
.min_inner_size(1024.0, 600.0)
.inner_size(1024.0, 600.0)
@ -643,7 +629,7 @@ fn hide_window(handle: &tauri::AppHandle) -> tauri::Result<()> {
handle
.tray_handle()
.get_item("toggle")
.set_title(format!("Show {}", app_title()))?;
.set_title(format!("Show {}", handle.package_info().name))?;
match get_window(handle) {
Some(window) => {

View File

@ -221,7 +221,6 @@ fn test_simple() -> Result<()> {
offset: None,
range: Range { start: 0, end: 10 },
});
println!("{:?}", search_result1);
assert!(search_result1.is_ok());
let search_result1 = search_result1.unwrap();
assert_eq!(search_result1.total, 1);

View File

@ -14,7 +14,7 @@
"all": false,
"shell": {
"all": false,
"open": true
"open": "^(https://)|(mailto:)?."
},
"dialog": {
"all": false,

View File

@ -15,7 +15,7 @@
"csp": {
"default-src": "'self'",
"img-src": "'self' asset: https://asset.localhost",
"connect-src": "'self' https://eu.posthog.com https://app.gitbutler.com ws://localhost:7703",
"connect-src": "'self' https://eu.posthog.com https://app.gitbutler.com https://o4504644069687296.ingest.sentry.io ws://localhost:7703",
"script-src": "'self' https://eu.posthog.com",
"style-src": "'self' 'unsafe-inline'"
}

View File

@ -95,5 +95,5 @@ input:focus {
padding: 24px;
}
.xterm-screen {
wdith: 100% !important;
width: 100% !important;
}

View File

@ -1,9 +1,19 @@
import type { HandleClientError } from '@sveltejs/kit';
import { handleErrorWithSentry, init } from '@sentry/sveltekit';
import type { NavigationEvent } from '@sveltejs/kit';
import { dev } from '$app/environment';
import { log } from '$lib';
// This will catch errors in load functions from +page.ts files
export const handleError = (({ error, event }: { error: any; event: any }) => {
console.error(error, event);
return {
message: error.message
};
}) satisfies HandleClientError;
init({
enabled: !dev,
dsn: 'https://9d407634d26b4d30b6a42d57a136d255@o4504644069687296.ingest.sentry.io/4504649768108032',
environment: dev ? 'development' : 'production',
tracesSampleRate: 1.0
});
log.info(`sentry init`);
const myErrorHandler = ({ error, event }: { error: any; event: NavigationEvent }) => {
console.error('An error occurred on the client side:', error, event);
};
export const handleError = handleErrorWithSentry(myErrorHandler);

View File

@ -0,0 +1,2 @@
export { default as Api } from './api';
export type { User, LoginToken, Project } from './api';

2
src/lib/api/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './ipc';
export { Api as CloudApi, type User, type LoginToken } from './cloud';

View File

@ -1,8 +1,7 @@
import { log } from '$lib';
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { writable, type Readable } from 'svelte/store';
import { clone } from './utils';
import { clone } from '$lib/utils';
import { writable } from 'svelte/store';
export type OperationDelete = { delete: [number, number] };
export type OperationInsert = { insert: [number, string] };
@ -19,11 +18,6 @@ export namespace Operation {
export type Delta = { timestampMs: number; operations: Operation[] };
export type DeltasEvent = {
deltas: Delta[];
filePath: string;
};
const cache: Record<string, Record<string, Promise<Record<string, Delta[]>>>> = {};
export const list = async (params: { projectId: string; sessionId: string; paths?: string[] }) => {
@ -53,22 +47,27 @@ export const list = async (params: { projectId: string; sessionId: string; paths
);
};
export default async (params: { projectId: string; sessionId: string }) => {
const init = await list(params);
const store = writable<Record<string, Delta[]>>(init);
appWindow.listen<DeltasEvent>(
export const subscribe = (
params: { projectId: string; sessionId: string },
callback: (params: {
projectId: string;
sessionId: string;
filePath: string;
deltas: Delta[];
}) => Promise<void> | void
) =>
appWindow.listen<{ deltas: Delta[]; filePath: string }>(
`project://${params.projectId}/sessions/${params.sessionId}/deltas`,
(event) => {
log.info(
`Received deltas for ${params.projectId}, ${params.sessionId}, ${event.payload.filePath}`
);
store.update((deltas) => ({
...deltas,
[event.payload.filePath]: event.payload.deltas
}));
}
(event) => callback({ ...params, ...event.payload })
);
return store as Readable<Record<string, Delta[]>>;
export const Deltas = async (params: { projectId: string; sessionId: string }) => {
const store = writable(await list(params));
subscribe(params, ({ filePath, deltas }) => {
store.update((deltasCache) => {
deltasCache[filePath] = deltas;
return deltasCache;
});
});
return { subscribe: store.subscribe };
};

31
src/lib/api/ipc/files.ts Normal file
View File

@ -0,0 +1,31 @@
import { invoke } from '@tauri-apps/api';
import { clone } from '$lib/utils';
const cache: Record<string, Record<string, Promise<Record<string, string>>>> = {};
export const list = async (params: { projectId: string; sessionId: string; paths?: string[] }) => {
const sessionFilesCache = cache[params.projectId] || {};
if (params.sessionId in sessionFilesCache) {
return sessionFilesCache[params.sessionId].then((files) =>
Object.fromEntries(
Object.entries(clone(files)).filter(([path]) =>
params.paths ? params.paths.includes(path) : true
)
)
);
}
const promise = invoke<Record<string, string>>('list_session_files', {
sessionId: params.sessionId,
projectId: params.projectId
});
sessionFilesCache[params.sessionId] = promise;
cache[params.projectId] = sessionFilesCache;
return promise.then((files) =>
Object.fromEntries(
Object.entries(clone(files)).filter(([path]) =>
params.paths ? params.paths.includes(path) : true
)
)
);
};

View File

@ -0,0 +1,28 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { get, writable } from 'svelte/store';
export type Activity = {
type: string;
timestampMs: number;
message: string;
};
export const list = (params: { projectId: string; startTimeMs?: number }) =>
invoke<Activity[]>('git_activity', params);
export const subscribe = (
params: { projectId: string },
callback: (params: { projectId: string }) => Promise<void> | void
) => appWindow.listen(`project://${params.projectId}/git/activity`, () => callback(params));
export const Activities = async (params: { projectId: string }) => {
const store = writable<Activity[]>(await list(params));
subscribe(params, async () => {
const activity = get(store);
const startTimeMs = activity.at(-1)?.timestampMs;
const newActivities = await list({ projectId: params.projectId, startTimeMs });
store.update((activities) => [...activities, ...newActivities]);
});
return { subscribe: store.subscribe };
};

View File

@ -0,0 +1,13 @@
import { invoke } from '@tauri-apps/api';
import { writable } from 'svelte/store';
import { sessions, git } from '$lib/api';
const list = (params: { projectId: string }) =>
invoke<Record<string, string>>('git_wd_diff', params);
export const Diffs = async (params: { projectId: string }) => {
const store = writable(await list(params));
git.activities.subscribe(params, ({ projectId }) => list({ projectId }).then(store.set));
sessions.subscribe(params, () => list(params).then(store.set));
return { subscribe: store.subscribe };
};

View File

@ -0,0 +1,19 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { derived, writable } from 'svelte/store';
export const get = (params: { projectId: string }) => invoke<string>('git_head', params);
export const subscribe = (
params: { projectId: string },
callback: (params: { projectId: string; head: string }) => Promise<void> | void
) =>
appWindow.listen<{ head: string }>(`project://${params.projectId}/git/head`, (event) =>
callback({ ...params, ...event.payload })
);
export const Head = async (params: { projectId: string }) => {
const store = writable(await get(params));
subscribe(params, ({ head }) => store.set(head));
return derived(store, (head) => head.replace('refs/heads/', ''));
};

View File

@ -1,7 +1,12 @@
import { invoke } from '@tauri-apps/api';
export * as statuses from './statuses';
export { Status } from './statuses';
export * as activities from './activities';
export type { Activity } from './activities';
export * as heads from './heads';
export * as diffs from './diffs';
export * as indexes from './indexes';
export { default as statuses } from './statuses';
export { default as activity } from './activity';
import { invoke } from '@tauri-apps/api';
export const commit = (params: { projectId: string; message: string; push: boolean }) =>
invoke<boolean>('git_commit', params);

View File

@ -0,0 +1,6 @@
import { appWindow } from '@tauri-apps/api/window';
export const subscribe = (
params: { projectId: string },
callback: (params: { projectId: string }) => Promise<void>
) => appWindow.listen(`project://${params.projectId}/git/activity`, () => callback({ ...params }));

View File

@ -1,6 +1,6 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { writable, type Readable } from 'svelte/store';
import { writable } from 'svelte/store';
import { sessions, git } from '$lib/api';
type FileStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'typeChange' | 'other';
@ -16,22 +16,13 @@ export namespace Status {
'unstaged' in status && status.unstaged !== null;
}
const list = (params: { projectId: string }) =>
export const list = (params: { projectId: string }) =>
invoke<Record<string, Status>>('git_status', params);
export default async (params: { projectId: string }) => {
const statuses = await list(params);
const store = writable(statuses);
[
`project://${params.projectId}/git/index`,
`project://${params.projectId}/git/activity`,
`project://${params.projectId}/sessions`
].forEach((eventName) => {
appWindow.listen(eventName, async () => {
store.set(await list(params));
});
});
return store as Readable<Record<string, Status>>;
export const Statuses = async (params: { projectId: string }) => {
const store = writable(await list(params));
sessions.subscribe(params, () => list(params).then(store.set));
git.activities.subscribe(params, () => list(params).then(store.set));
git.indexes.subscribe(params, () => list(params).then(store.set));
return { subscribe: store.subscribe };
};

12
src/lib/api/ipc/index.ts Normal file
View File

@ -0,0 +1,12 @@
export * as git from './git';
export { Status, type Activity } from './git';
export * as deltas from './deltas';
export { type Delta, Operation } from './deltas';
export * as sessions from './sessions';
export { Session } from './sessions';
export * as users from './users';
export * as projects from './projects';
export type { Project } from './projects';
export * as searchResults from './search';
export { type SearchResult } from './search';
export * as files from './files';

View File

@ -1,6 +1,6 @@
import { invoke } from '@tauri-apps/api';
import { derived, writable } from 'svelte/store';
import type { Project as ApiProject } from '$lib/api';
import type { Project as ApiProject } from '$lib/api/cloud';
import { derived, readable, writable } from 'svelte/store';
export type Project = {
id: string;
@ -9,9 +9,9 @@ export type Project = {
api: ApiProject & { sync: boolean };
};
const list = () => invoke<Project[]>('list_projects');
export const list = () => invoke<Project[]>('list_projects');
const update = (params: {
export const update = (params: {
project: {
id: string;
title?: string;
@ -19,14 +19,12 @@ const update = (params: {
};
}) => invoke<Project>('update_project', params);
const add = (params: { path: string }) => invoke<Project>('add_project', params);
export const add = (params: { path: string }) => invoke<Project>('add_project', params);
const del = (params: { id: string }) => invoke('delete_project', params);
export default async () => {
const init = await list();
const store = writable<Project[]>(init);
export const del = (params: { id: string }) => invoke('delete_project', params);
export const Projects = async () => {
const store = writable(await list());
return {
subscribe: store.subscribe,
get: (id: string) => {

View File

@ -10,7 +10,7 @@ export type SearchResult = {
highlighted: string[];
};
export const search = (params: {
export const list = (params: {
projectId: string;
query: string;
limit?: number;

View File

@ -0,0 +1,63 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { clone } from '$lib/utils';
import { writable } from 'svelte/store';
export namespace Session {
export const within = (session: Session | undefined, timestampMs: number) => {
if (!session) return false;
const { startTimestampMs, lastTimestampMs } = session.meta;
return startTimestampMs <= timestampMs && timestampMs <= lastTimestampMs;
};
}
export type Session = {
id: string;
hash?: string;
meta: {
startTimestampMs: number;
lastTimestampMs: number;
branch?: string;
commit?: string;
};
};
const cache: Record<string, Promise<Session[]>> = {};
export const list = async (params: { projectId: string; earliestTimestampMs?: number }) => {
if (params.projectId in cache) {
return cache[params.projectId].then((sessions) =>
clone(sessions).filter((s) =>
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
)
);
}
cache[params.projectId] = invoke<Session[]>('list_sessions', {
projectId: params.projectId
});
return cache[params.projectId].then((sessions) =>
clone(sessions).filter((s) =>
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
)
);
};
export const subscribe = (
params: { projectId: string },
callback: (params: { projectId: string; session: Session }) => Promise<void> | void
) =>
appWindow.listen<Session>(`project://${params.projectId}/sessions`, async (event) =>
callback({ ...params, session: event.payload })
);
export const Sessions = async (params: { projectId: string }) => {
const store = writable(await list(params));
subscribe(params, ({ session }) => {
store.update((sessions) => {
const index = sessions.findIndex((s) => s.id === session.id);
if (index === -1) return [...sessions, session];
sessions[index] = session;
return sessions;
});
});
return { subscribe: store.subscribe };
};

View File

@ -1,18 +1,15 @@
import type { User } from '$lib/api';
import { writable } from 'svelte/store';
import { invoke } from '@tauri-apps/api';
import { writable } from 'svelte/store';
const get = () => invoke<User | undefined>('get_user');
export const get = () => invoke<User | undefined>('get_user');
const set = (params: { user: User }) => invoke<void>('set_user', params);
export const set = (params: { user: User }) => invoke<void>('set_user', params);
const del = () => invoke<void>('delete_user');
export const del = () => invoke<void>('delete_user');
export default async () => {
const store = writable<User | undefined>(undefined);
const init = await get();
store.set(init);
export const CurrentUser = async () => {
const store = writable<User | undefined>(await get());
return {
subscribe: store.subscribe,
set: async (user: User) => {

View File

@ -1,5 +1,5 @@
<script lang="ts">
import type { Project } from '$lib/projects';
import type { Project } from '$lib/api';
import type { Readable } from 'svelte/store';
import { IconHome } from './icons';
import { Tooltip } from '$lib/components';

View File

@ -94,6 +94,11 @@
text-underline-offset: 3px;
}
a:focus,
button:focus {
@apply outline-none;
}
.basic {
@apply text-zinc-300;
}

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { HighlightStyle, LanguageSupport } from '@codemirror/language';
import { HighlightStyle, type LanguageSupport } from '@codemirror/language';
import { tags, highlightTree } from '@lezer/highlight';
import { NodeType, Tree } from '@lezer/common';
import { javascript } from '@codemirror/lang-javascript';

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { type Delta, Operation } from '$lib/deltas';
import { type Delta, Operation } from '$lib/api';
import { lineDiff } from './diff';
import { create } from './CodeHighlighter';
import { buildDiffRows, documentMap, RowType, type Row } from './renderer';

View File

@ -1,14 +1,16 @@
<script lang="ts">
import tinykeys from 'tinykeys';
import type { Project } from '$lib/projects';
import type { Project } from '$lib/api';
import { derived, readable, writable, type Readable } from 'svelte/store';
import { Modal } from '$lib/components';
import listAvailableCommands, { Action, type Group } from './commands';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { open } from '@tauri-apps/api/shell';
import { IconExternalLink } from '../icons';
export let projects: Readable<Project[]>;
export let addProject: (params: { path: string }) => Promise<Project>;
export let project = readable<Project | undefined>(undefined);
const input = writable('');
@ -21,37 +23,74 @@
([projects, project, input, scopeToProject, selectedGroup]) =>
selectedGroup !== undefined
? [selectedGroup]
: listAvailableCommands({ projects, project: scopeToProject ? project : undefined, input })
: listAvailableCommands({
addProject,
projects,
project: scopeToProject ? project : undefined,
input
})
);
let selection = [0, 0] as [number, number];
const selection = writable<[number, number]>([0, 0]);
commandGroups.subscribe((groups) => {
const newGroupIndex = Math.min(selection[0], groups.length - 1);
const newGroupIndex = Math.min($selection[0], groups.length - 1);
Promise.resolve(groups[newGroupIndex]).then((group) => {
const newCommandIndex = Math.min(selection[1], group.commands.length - 1);
selection = [newGroupIndex, newCommandIndex];
const newCommandIndex = Math.min($selection[1], group.commands.length - 1);
$selection = [newGroupIndex, newCommandIndex];
});
});
const selectNextCommand = () => {
selection.subscribe(() => {
const selected = document.querySelector('.selected');
if (selected) {
selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
});
const selectNextCommand = async () => {
if (!modal?.isOpen()) return;
Promise.resolve($commandGroups[selection[0]]).then((group) => {
if (selection[1] < group.commands.length - 1) {
selection = [selection[0], selection[1] + 1];
} else if (selection[0] < $commandGroups.length - 1) {
selection = [selection[0] + 1, 0];
}
});
const group = await Promise.resolve($commandGroups[$selection[0]]);
const nextCommandIndex = group.commands.findIndex((_command, index) => index > $selection[1]);
if (nextCommandIndex > -1) {
$selection = [$selection[0], nextCommandIndex];
} else {
await selectNextGroup();
}
};
const selectPreviousCommand = () => {
const selectNextGroup = async () => {
if (!modal?.isOpen()) return;
if (selection[1] > 0) {
selection = [selection[0], selection[1] - 1];
} else if (selection[0] > 0) {
Promise.resolve($commandGroups[selection[0] - 1]).then((previousGroup) => {
selection = [selection[0] - 1, previousGroup.commands.length - 1];
});
const groups = await Promise.all($commandGroups.map((group) => Promise.resolve(group)));
const nextGroupIndex = groups.findIndex(
(group, index) => index > $selection[0] && group.commands.length > 0
);
if (nextGroupIndex > -1) {
$selection = [nextGroupIndex, 0];
}
};
const selectPreviousCommand = async () => {
if (!modal?.isOpen()) return;
const group = await Promise.resolve($commandGroups[$selection[0]]);
const previousCommandIndex = group.commands
.map((_command, index) => index < $selection[1])
.lastIndexOf(true);
if (previousCommandIndex > -1) {
$selection = [$selection[0], previousCommandIndex];
} else {
await selectPreviousGroup();
}
};
const selectPreviousGroup = async () => {
if (!modal?.isOpen()) return;
const groups = await Promise.all($commandGroups.map((group) => Promise.resolve(group)));
const previousGroupIndex = groups
.map((group, index) => index < $selection[0] && group.commands.length > 0)
.lastIndexOf(true);
if (previousGroupIndex > -1) {
$selection = [previousGroupIndex, groups[previousGroupIndex].commands.length - 1];
}
};
@ -61,9 +100,12 @@
action.href.startsWith('http') || action.href.startsWith('mailto')
? open(action.href)
: goto(action.href);
modal?.hide();
modal?.close();
} else if (Action.isGroup(action)) {
selectedGroup.set(action);
} else if (Action.isRun(action)) {
action();
modal?.close();
}
scopeToProject.set(!!$project);
};
@ -74,6 +116,7 @@
input.set('');
scopeToProject.set(!!$project);
selectedGroup.set(undefined);
$selection = [0, 0];
};
export const show = () => {
@ -97,8 +140,8 @@
'Control+p': selectPreviousCommand,
Enter: () => {
if (!modal?.isOpen()) return;
Promise.resolve($commandGroups[selection[0]]).then((group) =>
trigger(group.commands[selection[1]].action)
Promise.resolve($commandGroups[$selection[0]]).then((group) =>
trigger(group.commands[$selection[1]].action)
);
}
})
@ -115,11 +158,9 @@
if (command.hotkey) {
unregisterCommandHotkeys.push(
tinykeys(window, {
[command.hotkey]: (event: KeyboardEvent) => {
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
// only trigger if the modal is visible
modal?.isOpen() && trigger(command.action);
[command.hotkey]: () => {
if (!modal?.isOpen()) return;
trigger(command.action);
}
})
);
@ -132,78 +173,90 @@
</script>
<Modal bind:this={modal}>
<div
class="command-palette flex max-h-[400px] min-h-[40px] w-[640px] flex-col rounded text-zinc-400"
>
<!-- Search input area -->
<header class="search-input-container flex items-center border-b border-zinc-400/20 py-2">
<div class="ml-4 mr-2 flex w-full items-center gap-1 text-lg text-zinc-300">
<!-- Project scope -->
{#if $scopeToProject && $project}
<span class="py-2 font-semibold">
{$project.title}
</span>
<span>/</span>
{/if}
{#if $selectedGroup}
<span class="font-semibold">
{$selectedGroup.title}
</span>
{:else}
<!-- svelte-ignore a11y-autofocus -->
<input
spellcheck="false"
class="command-palette-input-field"
bind:value={$input}
type="text"
autofocus
placeholder={!$project
? 'Search for repositories'
: 'Search for commands, files and code changes...'}
/>
{/if}
</div>
</header>
<div class="h-[400px]">
<div
class="command-palette flex max-h-[400px] min-h-[40px] w-[640px] flex-col rounded rounded-lg border-[0.5px] border-[#3F3F3f] bg-zinc-900/70 p-0 text-zinc-400 shadow-lg backdrop-blur-lg"
>
<!-- Search input area -->
<header class="search-input-container flex items-center border-b border-zinc-400/20 py-2">
<div class="ml-4 mr-2 flex w-full items-center gap-1 text-lg text-zinc-300">
<!-- Project scope -->
{#if $scopeToProject && $project}
<span class="py-2 font-semibold">
{$project.title}
</span>
<span>/</span>
{/if}
{#if $selectedGroup}
<span class="font-semibold">
{$selectedGroup.title}
</span>
{:else}
<!-- svelte-ignore a11y-autofocus -->
<input
spellcheck="false"
class="command-palette-input-field"
bind:value={$input}
type="text"
autofocus
placeholder={!$project
? 'Search your projects'
: 'Search for commands, files and code changes'}
/>
{/if}
</div>
</header>
<!-- Command list -->
<ul class="command-pallete-content-container flex-auto overflow-y-auto pb-2">
{#each $commandGroups as group, groupIdx}
{#await group then group}
<li class="w-full cursor-default select-none px-2">
<header class="command-palette-section-header result-section-header">
<span>{group.title}</span>
{#if group.description}
<span class="ml-2 font-light italic text-zinc-300/70">({group.description})</span>
{/if}
</header>
<!-- Command list -->
<ul class="command-pallete-content-container flex-auto overflow-y-auto pb-2">
{#each $commandGroups as group, groupIdx}
{#await group then group}
<li
class="w-full cursor-default select-none px-2"
class:hidden={group.commands.length === 0}
>
<header class="command-palette-section-header result-section-header">
<span>{group.title}</span>
{#if group.description}
<span class="ml-2 font-light italic text-zinc-300/70">({group.description})</span>
{/if}
</header>
<ul class="quick-command-list flex flex-col text-zinc-300">
{#each group.commands as command, commandIdx}
<li
class="quick-command-item flex w-full cursor-default rounded-lg"
class:selected={selection[0] === groupIdx && selection[1] === commandIdx}
>
<button
on:mouseover={() => (selection = [groupIdx, commandIdx])}
on:focus={() => (selection = [groupIdx, commandIdx])}
on:click={() => trigger(command.action)}
class="text-color-500 flex w-full items-center gap-2 rounded-lg p-2 px-2 outline-none"
<ul class="quick-command-list flex flex-col text-zinc-300">
{#each group.commands as command, commandIdx}
<li
class="quick-command-item flex w-full cursor-default rounded-lg"
class:selected={$selection[0] === groupIdx && $selection[1] === commandIdx}
>
<svelte:component this={command.icon} class="icon h-5 w-5 text-zinc-500 " />
<span class="quick-command flex-1 text-left font-medium">{command.title}</span>
{#if command.hotkey}
{#each command.hotkey.split('+') as key}
<span class="quick-command-key">{key}</span>
{/each}
{/if}
</button>
</li>
{/each}
</ul>
</li>
{/await}
{/each}
</ul>
<button
on:mouseover={() => ($selection = [groupIdx, commandIdx])}
on:focus={() => ($selection = [groupIdx, commandIdx])}
on:click={() => trigger(command.action)}
class="text-color-500 flex w-full items-center gap-2 rounded-lg p-2 px-2 outline-none"
>
<svelte:component this={command.icon} class="icon h-5 w-5 text-zinc-500 " />
<span
class="quick-command flex flex-1 items-center gap-1 text-left font-medium"
>
{command.title}
{#if Action.isExternalLink(command.action)}
<IconExternalLink class="h-4 w-4 text-zinc-600" />
{/if}
</span>
{#if command.hotkey}
{#each command.hotkey.replace('Meta', '⌘').split('+') as key}
<span class="quick-command-key">{key}</span>
{/each}
{/if}
</button>
</li>
{/each}
</ul>
</li>
{/await}
{/each}
</ul>
</div>
</div>
</Modal>

View File

@ -1,22 +1,38 @@
import type { Project } from '$lib/projects';
import { GitCommitIcon, IconFile, IconProject, IconTerminal, RewindIcon, FileIcon } from '../icons';
import { matchFiles } from '$lib/git';
import { type Project, git } from '$lib/api';
import { open } from '@tauri-apps/api/dialog';
import { toasts } from '$lib';
import {
IconGitCommit,
IconFile,
IconFeedback,
IconProject,
IconTerminal,
IconSettings,
IconAdjustmentsHorizontal,
IconDiscord,
IconSearch,
IconRewind
} from '../icons';
import type { SvelteComponent } from 'svelte';
import { format, startOfISOWeek, startOfMonth, subDays, subMonths, subWeeks } from 'date-fns';
type ActionLink = {
href: string;
};
type ActionRun = () => void;
interface Newable<ReturnType> {
new (...args: any[]): ReturnType;
}
export type Action = ActionLink | Group;
export type Action = ActionLink | Group | ActionRun;
export namespace Action {
export const isLink = (action: Action): action is ActionLink => 'href' in action;
export const isExternalLink = (action: Action): action is ActionLink =>
isLink(action) && (action.href.startsWith('http') || action.href.startsWith('mailto'));
export const isGroup = (action: Action): action is Group => 'commands' in action;
export const isRun = (action: Action): action is ActionRun => typeof action === 'function';
}
export type Command = {
@ -32,98 +48,105 @@ export type Group = {
commands: Command[];
};
const goToProjectGroup = ({ projects, input }: { projects: Project[]; input: string }): Group => ({
title: 'Go to project',
commands: projects
.map((project) => ({
title: project.title,
action: {
href: `/projects/${project.id}/`
},
icon: IconProject
}))
.filter(({ title }) => input.length === 0 || title.toLowerCase().includes(input.toLowerCase()))
});
const actionsGroup = ({ project, input }: { project: Project; input: string }): Group => ({
title: 'Actions',
const projectsGroup = ({
addProject,
projects,
input
}: {
addProject: (params: { path: string }) => Promise<Project>;
projects: Project[];
input: string;
}): Group => ({
title: 'Projects',
commands: [
{
title: 'Commit',
hotkey: 'Shift+C',
action: {
href: `/projects/${project.id}/commit/`
},
icon: GitCommitIcon
title: 'New project...',
hotkey: 'Meta+Shift+N',
icon: IconProject,
action: async () => {
const selectedPath = await open({
directory: true,
recursive: true
});
if (selectedPath === null) return;
if (Array.isArray(selectedPath) && selectedPath.length !== 1) return;
const projectPath = Array.isArray(selectedPath) ? selectedPath[0] : selectedPath;
try {
addProject({ path: projectPath });
} catch (e: any) {
toasts.error(e.message);
}
}
},
...projects
.filter(
({ title }) => input.length === 0 || title.toLowerCase().includes(input.toLowerCase())
)
.map((project, i) => ({
title: project.title,
hotkey: `Meta+${i + 1}`,
action: {
href: `/projects/${project.id}/`
},
icon: IconProject
})),
{
title: 'Terminal',
hotkey: 'Shift+T',
title: 'Search all repositories',
action: {
href: `/projects/${project?.id}/terminal/`
href: '/'
},
icon: IconTerminal
},
{
title: 'Replay History',
action: {
title: 'Replay working history',
commands: [
icon: IconSearch
}
].filter(({ title }) => input.length === 0 || title.toLowerCase().includes(input.toLowerCase()))
});
const navigateGroup = ({ project, input }: { project?: Project; input: string }): Group => ({
title: 'Navigate',
commands: [
...(project
? [
{
title: 'Eralier today',
icon: RewindIcon,
hotkey: '1',
title: 'Commit',
hotkey: 'Meta+Shift+C',
action: {
href: `/projects/${project.id}/player/${format(new Date(), 'yyyy-MM-dd')}/`
}
href: `/projects/${project.id}/commit/`
},
icon: IconGitCommit
},
{
title: 'Yesterday',
icon: RewindIcon,
hotkey: '2',
title: 'Replay',
hotkey: 'Meta+R',
action: {
href: `/projects/${project.id}/player/${format(
subDays(new Date(), 1),
'yyyy-MM-dd'
)}/`
}
href: `/projects/${project.id}/player/`
},
icon: IconRewind
},
{
title: 'The day before yesterday',
icon: RewindIcon,
hotkey: '3',
title: 'Terminal',
hotkey: 'Meta+T',
action: {
href: `/projects/${project.id}/player/${format(
subDays(new Date(), 2),
'yyyy-MM-dd'
)}/`
}
href: `/projects/${project?.id}/terminal/`
},
icon: IconTerminal
},
{
title: 'The beginning of last week',
icon: RewindIcon,
hotkey: '4',
title: 'Project settings',
hotkey: 'Meta+Shift+,',
action: {
href: `/projects/${project.id}/player/${format(
startOfISOWeek(subWeeks(new Date(), 1)),
'yyyy-MM-dd'
)}/`
}
},
{
title: 'The beginning of last month',
icon: RewindIcon,
hotkey: '5',
action: {
href: `/projects/${project.id}/player/${format(
startOfMonth(subMonths(new Date(), 1)),
'yyyy-MM-dd'
)}/`
}
href: `/projects/${project?.id}/settings/`
},
icon: IconSettings
}
]
]
: []),
{
title: 'Settings',
hotkey: 'Meta+,',
action: {
href: '/users/'
},
icon: RewindIcon
icon: IconAdjustmentsHorizontal
}
].filter(({ title }) => input.length === 0 || title.toLowerCase().includes(input.toLowerCase()))
});
@ -141,7 +164,7 @@ const fileGroup = ({
description: 'type part of a file name',
commands: []
}
: matchFiles({ projectId: project.id, matchPattern: input }).then((files) => ({
: git.matchFiles({ projectId: project.id, matchPattern: input }).then((files) => ({
title: 'Files',
description: files.length === 0 ? `no files containing '${input}'` : '',
commands: files.map((file) => ({
@ -161,24 +184,36 @@ const supportGroup = ({ input }: { input: string }): Group => ({
action: {
href: `https://docs.gitbutler.com`
},
icon: FileIcon
icon: IconFile
},
{
title: 'Discord',
action: {
href: `https://discord.gg/MmFkmaJ42D`
},
icon: GitCommitIcon
icon: IconDiscord
},
{
title: 'Send feedback',
action: {
href: 'mailto:hello@gitbutler.com'
},
icon: IconFeedback
}
].filter(({ title }) => input.length === 0 || title.toLowerCase().includes(input.toLowerCase()))
});
export default (params: { projects: Project[]; project?: Project; input: string }) => {
const { projects, input, project } = params;
export default (params: {
addProject: (params: { path: string }) => Promise<Project>;
projects: Project[];
project?: Project;
input: string;
}) => {
const { addProject, projects, input, project } = params;
const groups = [];
!project && groups.push(goToProjectGroup({ projects, input }));
project && groups.push(actionsGroup({ project, input }));
groups.push(navigateGroup({ project, input }));
!project && groups.push(projectsGroup({ addProject, projects, input }));
project && groups.push(fileGroup({ project, input }));
groups.push(supportGroup({ input }));

View File

@ -5,12 +5,21 @@
import Button from '../Button/Button.svelte';
let dialog: Dialog;
let count = 0;
</script>
<Meta title="GitButler/Dialog" component={Dialog} />
<Story name="Dialog with title only">
<Button on:click={() => dialog.show()}>Open Dialog</Button>
<Button on:click={dialog.show}>Open Dialog</Button>
<Dialog bind:this={dialog} />
</Story>
<Story name="Dialog with content">
<Button on:click={dialog.show}>Open Dialog</Button>
<Dialog bind:this={dialog}>
<p class="w-[346px]">
GitButler offers support for generating Git commits automatically. To use this feature, you
will need to sync with GitButler cloud.
</p>
</Dialog>
</Story>

View File

@ -4,29 +4,46 @@
import { IconClose } from '$lib/components/icons';
export const show = () => modal.show();
const hide = () => modal.hide();
let modal: Modal;
</script>
<Modal on:close bind:this={modal}>
<div class="flex flex-col text-zinc-400">
<div class="flex p-4">
<div class="flex-grow text-[18px] text-zinc-300">
<Modal bind:this={modal} let:close>
<div class="wrapper flex w-full flex-col text-zinc-300">
<header class="flex w-full justify-between gap-4 p-4">
<h2 class="text-[18px] ">
<slot name="title">Title</slot>
</h2>
<Button filled={false} on:click={close} icon={IconClose} />
</header>
{#if $$slots.default}
<div class="p-4 text-base ">
<slot />
</div>
<button on:click={() => modal.hide()}>
<IconClose class="h-6 w-6" />
</button>
</div>
<p class="p-4 text-base">
<slot />
</p>
<div class="m-4 ml-auto flex gap-4">
<slot name="controls" {hide} {show}>
<Button filled on:click={hide}>Cancel</Button>
<Button filled role="primary" on:click={hide}>Confirm</Button>
{/if}
<footer class="flex w-full justify-end gap-4 p-4">
<slot name="controls" {close}>
<Button filled={false} outlined={true} on:click={close}>Secondary action</Button>
<Button role="primary" on:click={close}>Primary action</Button>
</slot>
</div>
</footer>
</div>
</Modal>
<style>
.wrapper {
background: linear-gradient(0deg, rgba(43, 43, 48, 0.8), rgba(43, 43, 48, 0.8)),
linear-gradient(0deg, rgba(63, 63, 63, 0.5), rgba(63, 63, 63, 0.5));
}
header {
box-shadow: inset 0px -1px 0px rgba(0, 0, 0, 0.1);
}
footer {
box-shadow: inset 0px 1px 0px rgba(0, 0, 0, 0.1);
}
</style>

View File

@ -1,12 +1,10 @@
<script lang="ts">
import type Users from '$lib/users';
import type Api from '$lib/api';
import type { LoginToken } from '$lib/api';
import type { LoginToken, CloudApi, users } from '$lib/api';
import { derived, writable } from 'svelte/store';
import { open } from '@tauri-apps/api/shell';
export let user: Awaited<ReturnType<typeof Users>>;
export let api: Awaited<ReturnType<typeof Api>>;
export let user: Awaited<ReturnType<typeof users.CurrentUser>>;
export let api: Awaited<ReturnType<typeof CloudApi>>;
const pollForUser = async (token: string) => {
const apiUser = await api.login.user.get(token).catch(() => null);

View File

@ -1,64 +1,42 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { scale } from 'svelte/transition';
let dialog: HTMLDialogElement;
let content: HTMLDivElement | null = null;
const dispatch = createEventDispatcher<{ close: void }>();
let open = false;
export const show = () => {
open = true;
dialog.showModal();
};
export const hide = () => {
open = false;
dialog.close();
dispatch('close');
open = true;
};
export const isOpen = () => open;
const handleClick = (event: MouseEvent) => {
if (content && !content.contains(event.target as Node | null) && !event.defaultPrevented) {
hide();
}
export const close = () => {
dialog.close();
open = false;
};
const handleClick = (event: Event) => {
if (event.defaultPrevented) return;
if (!dialog?.open) return;
const isClickInside = !content || content.contains(event.target as Node | null);
if (isClickInside) return;
close();
};
</script>
<!--
@component
In most cases, you should use the Dialog component, which builds on top of this, instead of this one.
This is a base Modal component which makes sure that all mouse and keyboard events are handled correctly.
It does minimal styling. A close event is fired when the modal is closed.
- Usage:
```tsx
<Modal>
your content slotted in
</Modal>
```
-->
<!-- test -->
<svelte:window on:click={handleClick} />
<dialog
class="my-0 overflow-hidden bg-transparent p-0"
on:click={handleClick}
on:keydown={handleClick}
class="bg-transparent"
in:scale={{ duration: 150 }}
bind:this={dialog}
on:close={hide}
on:close={close}
>
{#if open}
<div class="modal-overlay relative top-[25%] h-[100vh] overflow-hidden">
<div
class="modal w-[640px] overflow-hidden rounded-lg border-[0.5px] border-[#3F3F3f] bg-zinc-900/70 p-0 shadow-lg backdrop-blur-lg"
>
<div class="flex" bind:this={content}>
<slot />
</div>
</div>
<div bind:this={content} class="flex">
<slot {close} isOpen={open} />
</div>
{/if}
</dialog>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { collapsable } from '$lib/paths';
import { Status } from '$lib/git/statuses';
import { Status } from '$lib/api';
export let statuses: Record<string, Status>;
</script>

View File

@ -1,14 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 341 B

View File

@ -1,14 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 6.878V6a2.25 2.25 0 012.25-2.25h7.5A2.25 2.25 0 0118 6v.878m-12 0c.235-.083.487-.128.75-.128h10.5c.263 0 .515.045.75.128m-12 0A2.25 2.25 0 004.5 9v.878m13.5-3A2.25 2.25 0 0119.5 9v.878m0 0a2.246 2.246 0 00-.75-.128H5.25c-.263 0-.515.045-.75.128m15 0A2.25 2.25 0 0121 12v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6c0-.98.626-1.813 1.5-2.122"
/>
</svg>

Before

Width:  |  Height:  |  Size: 565 B

View File

@ -1,14 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 01-1.043 3.296 3.745 3.745 0 01-3.296 1.043A3.745 3.745 0 0112 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 01-3.296-1.043 3.745 3.745 0 01-1.043-3.296A3.745 3.745 0 013 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 011.043-3.296 3.746 3.746 0 013.296-1.043A3.746 3.746 0 0112 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 013.296 1.043 3.746 3.746 0 011.043 3.296A3.745 3.745 0 0121 12z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 674 B

View File

@ -1,14 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"
/>
</svg>

Before

Width:  |  Height:  |  Size: 459 B

View File

@ -1,21 +0,0 @@
<script lang="ts">
let className = '';
export { className as class };
</script>
<svg
class={className}
width="20"
height="20"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
/>
</svg>

View File

@ -1,14 +0,0 @@
<svg
class="h-6 w-6 flex-none text-zinc-500"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 10.5v6m3-3H9m4.06-7.19l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 416 B

View File

@ -1,14 +0,0 @@
<svg
class="h-6 w-6 flex-none text-zinc-500"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5l-3.9 19.5m-2.1-19.5l-3.9 19.5"
/>
</svg>

Before

Width:  |  Height:  |  Size: 287 B

View File

@ -0,0 +1,24 @@
<script lang="ts">
let className = '';
export { className as class };
</script>
<svg
class={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M14 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M4 6l8 0" />
<path d="M16 6l4 0" />
<path d="M8 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M4 12l2 0" />
<path d="M10 12l10 0" />
<path d="M17 18m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M4 18l11 0" />
<path d="M19 18l1 0" />
</svg>

View File

@ -1,14 +0,0 @@
<svg
{...$$restProps}
width="15"
height="15"
viewBox="0 0 20 20"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 10C0 4.486 4.486 0 10 0C15.514 0 20 4.486 20 10C20 15.514 15.514 20 10 20C4.486 20 0 15.514 0 10ZM7.70711 6.29289C7.31658 5.90237 6.68342 5.90237 6.29289 6.29289C5.90237 6.68342 5.90237 7.31658 6.29289 7.70711L8.58579 10L6.29289 12.2929C5.90237 12.6834 5.90237 13.3166 6.29289 13.7071C6.68342 14.0976 7.31658 14.0976 7.70711 13.7071L10 11.4142L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166 14.0976 12.6834 13.7071 12.2929L11.4142 10L13.7071 7.70711C14.0976 7.31658 14.0976 6.68342 13.7071 6.29289C13.3166 5.90237 12.6834 5.90237 12.2929 6.29289L10 8.58579L7.70711 6.29289Z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 806 B

View File

@ -1,19 +0,0 @@
<svg
{...$$restProps}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M7 3.34a10 10 0 1 1 -4.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 4.995 -8.336z"
stroke-width="0"
fill="currentColor"
/>
</svg>

Before

Width:  |  Height:  |  Size: 411 B

View File

@ -1,19 +0,0 @@
<svg
{...$$restProps}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M12 3v18" />
<path d="M12 14l7 -7" />
<path d="M12 19l8.5 -8.5" />
<path d="M12 9l4.5 -4.5" />
</svg>

Before

Width:  |  Height:  |  Size: 436 B

View File

@ -0,0 +1,20 @@
<script lang="ts">
let className = '';
export { className as class };
</script>
<svg
class={className}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M14.983 3l.123 .006c2.014 .214 3.527 .672 4.966 1.673a1 1 0 0 1 .371 .488c1.876 5.315 2.373 9.987 1.451 12.28c-1.003 2.005 -2.606 3.553 -4.394 3.553c-.94 0 -2.257 -1.596 -2.777 -2.969l-.02 .005c.838 -.131 1.69 -.323 2.572 -.574a1 1 0 1 0 -.55 -1.924c-3.32 .95 -6.13 .95 -9.45 0a1 1 0 0 0 -.55 1.924c.725 .207 1.431 .373 2.126 .499l.444 .074c-.477 1.37 -1.695 2.965 -2.627 2.965c-1.743 0 -3.276 -1.555 -4.267 -3.644c-.841 -2.206 -.369 -6.868 1.414 -12.174a1 1 0 0 1 .358 -.49c1.392 -1.016 2.807 -1.475 4.717 -1.685a1 1 0 0 1 .938 .435l.063 .107l.652 1.288l.16 -.019c.877 -.09 1.718 -.09 2.595 0l.158 .019l.65 -1.287a1 1 0 0 1 .754 -.54l.123 -.01zm-5.983 6a2 2 0 0 0 -1.977 1.697l-.018 .154l-.005 .149l.005 .15a2 2 0 1 0 1.995 -2.15zm6 0a2 2 0 0 0 -1.977 1.697l-.018 .154l-.005 .149l.005 .15a2 2 0 1 0 1.995 -2.15z"
stroke-width="0"
fill="currentColor"
/>
</svg>

View File

@ -0,0 +1,18 @@
<script lang="ts">
let className = '';
export { className as class };
</script>
<svg
class={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6" />
<path d="M11 13l9 -9" />
<path d="M15 4h5v5" />
</svg>

View File

@ -0,0 +1,20 @@
<script lang="ts">
let className = '';
export { className as class };
</script>
<svg
class={className}
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.40002 6.80111e-06C1.07452 6.80111e-06 0 1.07453 0 2.40002V10.4C0 11.7255 1.07452 12.8 2.40002 12.8H4.80003V15.2C4.80003 15.495 4.96236 15.7661 5.22244 15.9054C5.48266 16.0445 5.7982 16.0293 6.04369 15.8656L10.6421 12.8H13.5998C14.9255 12.8 15.9998 11.7255 15.9998 10.4V2.40002C15.9998 1.07452 14.9255 0 13.5998 0L2.40002 6.80111e-06ZM1.59992 2.40002C1.59992 1.95818 1.95817 1.59993 2.40002 1.59993H13.5999C14.0417 1.59993 14.4 1.95818 14.4 2.40002V10.4C14.4 10.8418 14.0417 11.1999 13.5999 11.1999H10.3999C10.242 11.1999 10.0876 11.2467 9.95619 11.3343L6.39971 13.7052V12C6.39971 11.7879 6.31547 11.5843 6.16547 11.4343C6.01547 11.2843 5.81191 11.1999 5.59975 11.1999H2.39978C1.95793 11.1999 1.59968 10.8418 1.59968 10.4L1.59992 2.40002Z"
fill="currentColor"
/>
</svg>

View File

@ -1,19 +0,0 @@
<svg
{...$$restProps}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M18.364 4.636a9 9 0 0 1 .203 12.519l-.203 .21l-4.243 4.242a3 3 0 0 1 -4.097 .135l-.144 -.135l-4.244 -4.243a9 9 0 0 1 12.728 -12.728zm-6.364 3.364a3 3 0 1 0 0 6a3 3 0 0 0 0 -6z"
stroke-width="0"
fill="currentColor"
/>
</svg>

Before

Width:  |  Height:  |  Size: 505 B

View File

@ -1,25 +0,0 @@
<script lang="ts">
let className = '';
export { className as class };
</script>
<svg
class={className}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<path d="M5.63 7.16l0 .01" />
<path d="M4.06 11l0 .01" />
<path d="M4.63 15.1l0 .01" />
<path d="M7.16 18.37l0 .01" />
<path d="M11 19.94l0 .01" />
</svg>

View File

@ -0,0 +1,21 @@
<script lang="ts">
let className = '';
export { className as class };
</script>
<svg
class={className}
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
stroke-width="1"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M8 12a4 4 0 110-8 4 4 0 010 8zm9.707 4.293l-4.82-4.82A5.968 5.968 0 0014 8 6 6 0 002 8a6 6 0 006 6 5.968 5.968 0 003.473-1.113l4.82 4.82a.997.997 0 001.414 0 .999.999 0 000-1.414z"
fill="currentColor"
/>
</svg>

View File

@ -0,0 +1,20 @@
<script lang="ts">
let className = '';
export { className as class };
</script>
<svg
class={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>

View File

@ -1,19 +0,0 @@
<svg
{...$$restProps}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 2c-.218 0 -.432 .002 -.642 .005l-.616 .017l-.299 .013l-.579 .034l-.553 .046c-4.785 .464 -6.732 2.411 -7.196 7.196l-.046 .553l-.034 .579c-.005 .098 -.01 .198 -.013 .299l-.017 .616l-.004 .318l-.001 .324c0 .218 .002 .432 .005 .642l.017 .616l.013 .299l.034 .579l.046 .553c.464 4.785 2.411 6.732 7.196 7.196l.553 .046l.579 .034c.098 .005 .198 .01 .299 .013l.616 .017l.642 .005l.642 -.005l.616 -.017l.299 -.013l.579 -.034l.553 -.046c4.785 -.464 6.732 -2.411 7.196 -7.196l.046 -.553l.034 -.579c.005 -.098 .01 -.198 .013 -.299l.017 -.616l.005 -.642l-.005 -.642l-.017 -.616l-.013 -.299l-.034 -.579l-.046 -.553c-.464 -4.785 -2.411 -6.732 -7.196 -7.196l-.553 -.046l-.579 -.034a28.058 28.058 0 0 0 -.299 -.013l-.616 -.017l-.318 -.004l-.324 -.001z"
stroke-width="0"
fill="currentColor"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,15 +0,0 @@
<svg
class="h-6 w-6 flex-none text-zinc-500"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
</svg>

Before

Width:  |  Height:  |  Size: 501 B

View File

@ -1,14 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 460 B

View File

@ -1,21 +1,7 @@
export { default as BookmarkIcon } from './BookmarkIcon.svelte';
export { default as BranchIcon } from './BranchIcon.svelte';
export { default as CommitIcon } from './CommitIcon.svelte';
export { default as ContactIcon } from './ContactIcon.svelte';
export { default as FileIcon } from './FileIcon.svelte';
export { default as FolderIcon } from './FolderIcon.svelte';
export { default as LabelIcon } from './LabelIcon.svelte';
export { default as GitCommitIcon } from './GitCommitIcon.svelte';
export { default as ProjectIcon } from './ProjectIcon.svelte';
export { default as RewindIcon } from './RewindIcon.svelte';
export { default as IconRotateClockwise } from './IconRotateClockwise.svelte';
export { default as IconGitCommit } from './IconGitCommit.svelte';
export { default as IconRewind } from './IconRewind.svelte';
export { default as IconPlayerPauseFilled } from './IconPlayerPauseFilled.svelte';
export { default as IconPlayerPlayFilled } from './IconPlayerPlayFilled.svelte';
export { default as IconCircleHalf } from './IconCircleHalf.svelte';
export { default as IconSquareRoundedFilled } from './IconSquareRoundedFilled.svelte';
export { default as IconMapPinFilled } from './IconMapPinFilled.svelte';
export { default as IconCircleFilled } from './IconCircleFilled.svelte';
export { default as IconCircleCancel } from './IconCircleCancel.svelte';
export { default as IconChevronLeft } from './IconChevronLeft.svelte';
export { default as IconChevronRight } from './IconChevronRight.svelte';
export { default as IconFile } from './IconFile.svelte';
@ -25,3 +11,9 @@ export { default as IconLoading } from './IconLoading.svelte';
export { default as IconProject } from './IconProject.svelte';
export { default as IconTerminal } from './IconTerminal.svelte';
export { default as IconClose } from './IconClose.svelte';
export { default as IconSettings } from './IconSettings.svelte';
export { default as IconAdjustmentsHorizontal } from './IconAdjustmentsHorizontal.svelte';
export { default as IconDiscord } from './IconDiscord.svelte';
export { default as IconExternalLink } from './IconExternalLink.svelte';
export { default as IconFeedback } from './IconFeedback.svelte';
export { default as IconSearch } from './IconSearch.svelte';

View File

@ -1,27 +0,0 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { writable, type Readable } from 'svelte/store';
import { log } from '$lib';
export type Activity = {
type: string;
timestampMs: number;
message: string;
};
const list = (params: { projectId: string; startTimeMs?: number }) =>
invoke<Activity[]>('git_activity', params);
export default async (params: { projectId: string }) => {
const activity = await list(params);
const store = writable(activity);
appWindow.listen(`project://${params.projectId}/git/activity`, async () => {
log.info(`Status: Received git activity event, projectId: ${params.projectId}`);
const startTimeMs = activity.at(-1)?.timestampMs;
const newActivities = await list({ projectId: params.projectId, startTimeMs });
store.update((activities) => [...activities, ...newActivities]);
});
return store as Readable<Activity[]>;
};

View File

@ -1,24 +0,0 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { writable, type Readable } from 'svelte/store';
import { log } from '$lib';
const getDiffs = (params: { projectId: string }) =>
invoke<Record<string, string>>('git_wd_diff', params);
export default async (params: { projectId: string }) => {
const diffs = await getDiffs(params);
const store = writable(diffs);
appWindow.listen(`project://${params.projectId}/sessions`, async () => {
log.info(`Status: Received sessions event, projectId: ${params.projectId}`);
store.set(await getDiffs(params));
});
appWindow.listen(`project://${params.projectId}/git/index`, async () => {
log.info(`Status: Received git activity event, projectId: ${params.projectId}`);
store.set(await getDiffs(params));
});
return store as Readable<Record<string, string>>;
};

View File

@ -1,18 +0,0 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { derived, writable } from 'svelte/store';
import { log } from '$lib';
const list = (params: { projectId: string }) => invoke<string>('git_head', params);
export default async (params: { projectId: string }) => {
const head = await list(params);
const store = writable(head);
appWindow.listen<{ head: string }>(`project://${params.projectId}/git/head`, async (payload) => {
log.info(`Status: Received git head event, projectId: ${params.projectId}`);
store.set(payload.payload.head);
});
return derived(store, (head) => head.replace('refs/heads/', ''));
};

View File

@ -1,8 +1,6 @@
export * as deltas from './deltas';
export * as projects from './projects';
export * as api from './api';
export * as log from './log';
export * as toasts from './toasts';
export * as sessions from './sessions';
export { Toaster } from './toasts';
export * as week from './week';
export * as uisessions from './uisessions';
export * from './search';

21
src/lib/sentry.ts Normal file
View File

@ -0,0 +1,21 @@
import { setUser } from '@sentry/sveltekit';
import type { User } from '$lib/api';
import * as log from '$lib/log';
export default () => {
return {
identify: (user: User | undefined) => {
if (user) {
log.info(`sentry identify`);
setUser({
id: user.id.toString(),
email: user.email,
username: user.name
});
} else {
log.info(`sentry reset`);
setUser(null);
}
}
};
};

View File

@ -1,101 +0,0 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import { writable, type Readable } from 'svelte/store';
import { log } from '$lib';
import type { Activity } from './git/activity';
import { clone } from './utils';
export namespace Session {
export const within = (session: Session | undefined, timestampMs: number) => {
if (!session) return false;
const { startTimestampMs, lastTimestampMs } = session.meta;
return startTimestampMs <= timestampMs && timestampMs <= lastTimestampMs;
};
}
export type Session = {
id: string;
hash?: string;
meta: {
startTimestampMs: number;
lastTimestampMs: number;
branch?: string;
commit?: string;
};
activity: Activity[];
};
const filesCache: Record<string, Record<string, Promise<Record<string, string>>>> = {};
export const listFiles = async (params: {
projectId: string;
sessionId: string;
paths?: string[];
}) => {
const sessionFilesCache = filesCache[params.projectId] || {};
if (params.sessionId in sessionFilesCache) {
return sessionFilesCache[params.sessionId].then((files) =>
Object.fromEntries(
Object.entries(clone(files)).filter(([path]) =>
params.paths ? params.paths.includes(path) : true
)
)
);
}
const promise = invoke<Record<string, string>>('list_session_files', {
sessionId: params.sessionId,
projectId: params.projectId
});
sessionFilesCache[params.sessionId] = promise;
filesCache[params.projectId] = sessionFilesCache;
return promise.then((files) =>
Object.fromEntries(
Object.entries(clone(files)).filter(([path]) =>
params.paths ? params.paths.includes(path) : true
)
)
);
};
const sessionsCache: Record<string, Promise<Session[]>> = {};
const list = async (params: { projectId: string; earliestTimestampMs?: number }) => {
if (params.projectId in sessionsCache) {
return sessionsCache[params.projectId].then((sessions) =>
clone(sessions).filter((s) =>
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
)
);
}
sessionsCache[params.projectId] = invoke<Session[]>('list_sessions', {
projectId: params.projectId
});
return sessionsCache[params.projectId].then((sessions) =>
clone(sessions).filter((s) =>
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
)
);
};
export default async (params: { projectId: string; earliestTimestampMs?: number }) => {
const store = writable([] as Session[]);
list(params).then((sessions) => {
store.set(sessions);
});
appWindow.listen<Session>(`project://${params.projectId}/sessions`, async (event) => {
log.info(`Received sessions event, projectId: ${params.projectId}`);
const session = event.payload;
store.update((sessions) => {
const index = sessions.findIndex((session) => session.id === event.payload.id);
if (index === -1) {
return [...sessions, session];
} else {
return [...sessions.slice(0, index), session, ...sessions.slice(index + 1)];
}
});
});
return store as Readable<Session[]>;
};

View File

@ -1,4 +1,5 @@
import toast, { type ToastOptions, type ToastPosition } from 'svelte-french-toast';
export { Toaster } from 'svelte-french-toast';
const defaultOptions = {
position: 'bottom-center' as ToastPosition,

View File

@ -1,5 +1,4 @@
import type { Session } from '$lib/sessions';
import type { Delta } from '$lib/deltas';
import type { Session, Delta } from '$lib/api';
export type UISession = {
session: Session;

View File

@ -1,18 +1,18 @@
<script lang="ts">
import '../app.postcss';
import { open } from '@tauri-apps/api/dialog';
import { toasts, Toaster } from '$lib';
import tinykeys from 'tinykeys';
import { Toaster } from 'svelte-french-toast';
import type { LayoutData } from './$types';
import { BackForwardButtons, Button } from '$lib/components';
import Breadcrumbs from '$lib/components/Breadcrumbs.svelte';
import { BackForwardButtons, Button, CommandPalette, Breadcrumbs } from '$lib/components';
import { page } from '$app/stores';
import CommandPalette from '$lib/components/CommandPalette/CommandPalette.svelte';
import { derived } from 'svelte/store';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
export let data: LayoutData;
const { user, posthog, projects } = data;
const { user, posthog, projects, sentry } = data;
const project = derived([page, projects], ([page, projects]) =>
projects.find((project) => project.id === page.params.projectId)
@ -22,11 +22,28 @@
onMount(() =>
tinykeys(window, {
'Meta+k': () => commandPalette.show()
'Meta+k': () => commandPalette.show(),
'Meta+,': () => goto('/users/'),
'Meta+Shift+N': async () => {
const selectedPath = await open({
directory: true,
recursive: true
});
if (selectedPath === null) return;
if (Array.isArray(selectedPath) && selectedPath.length !== 1) return;
const projectPath = Array.isArray(selectedPath) ? selectedPath[0] : selectedPath;
try {
await projects.add({ path: projectPath });
} catch (e: any) {
toasts.error(e.message);
}
}
})
);
user.subscribe(posthog.identify);
user.subscribe(sentry.identify);
</script>
<div class="flex h-full max-h-full min-h-full flex-col">
@ -59,5 +76,5 @@
<slot />
</div>
<Toaster />
<CommandPalette bind:this={commandPalette} {projects} {project} />
<CommandPalette bind:this={commandPalette} {projects} {project} addProject={projects.add} />
</div>

View File

@ -1,27 +1,29 @@
import { readable } from 'svelte/store';
import type { LayoutLoad } from './$types';
import { building } from '$app/environment';
import type { Project } from '$lib/projects';
import Api from '$lib/api';
import type { Project } from '$lib/api';
import { Api } from '$lib/api/cloud';
import Posthog from '$lib/posthog';
import * as log from '$lib/log';
import Sentry from '$lib/sentry';
import { setup as setupLogger } from '$lib/log';
import { wrapLoadWithSentry } from '@sentry/sveltekit';
export const ssr = false;
export const prerender = true;
export const csr = true;
export const load: LayoutLoad = async ({ fetch }) => {
export const load: LayoutLoad = wrapLoadWithSentry(async ({ fetch }) => {
const projects = building
? {
...readable<Project[]>([]),
add: () => {
add: (params: { path: string }): Promise<Project> => {
throw new Error('not implemented');
},
get: () => {
throw new Error('not implemented');
}
}
: await (await import('$lib/projects')).default();
: await (await import('$lib/api')).projects.Projects();
const user = building
? {
...readable<undefined>(undefined),
@ -32,12 +34,13 @@ export const load: LayoutLoad = async ({ fetch }) => {
throw new Error('not implemented');
}
}
: await (await import('$lib/users')).default();
await log.setup();
: await (await import('$lib/api')).users.CurrentUser();
setupLogger();
return {
projects,
user,
api: Api({ fetch }),
posthog: Posthog()
posthog: Posthog(),
sentry: Sentry()
};
};
});

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { open } from '@tauri-apps/api/dialog';
import type { LayoutData } from './$types';
import { open } from '@tauri-apps/api/dialog';
import { toasts } from '$lib';
import { Button, Tooltip } from '$lib/components';

View File

@ -1,18 +1,16 @@
<script lang="ts">
import type { LayoutData } from './$types';
import type { Project } from '$lib/projects';
import type { Project } from '$lib/api';
import { Button, Tooltip } from '$lib/components';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { IconTerminal } from '$lib/components/icons';
import { IconSearch, IconSettings, IconTerminal } from '$lib/components/icons';
import { onMount } from 'svelte';
import tinykeys from 'tinykeys';
import { derived } from 'svelte/store';
import { format } from 'date-fns';
export let data: LayoutData;
const { project } = data;
$: statuses = derived(data.statuses, (statuses) => statuses);
let query: string;
@ -30,32 +28,12 @@
onMount(() =>
tinykeys(window, {
'Shift+c': (event: KeyboardEvent) => {
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
if (Object.keys($statuses).length === 0) return;
$project && goto(`/projects/${$project.id}/commit/`);
},
'Shift+t': (event: KeyboardEvent) => {
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
$project && goto(`/projects/${$project.id}/terminal/`);
},
'Shift+p': (event: KeyboardEvent) => {
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
$project && goto(`/projects/${$project.id}/`);
},
'Meta+Shift+p': (event: KeyboardEvent) => {
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
$project && goto(`/projects/${$project.id}/settings/`);
},
'Shift+r': (event: KeyboardEvent) => {
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
$project && goto(`/projects/${$project.id}/player/${format(new Date(), 'yyyy-MM-dd')}`);
},
'Meta+Shift+C': () => $project && goto(`/projects/${$project.id}/commit/`),
'Meta+T': () => $project && goto(`/projects/${$project.id}/terminal/`),
'Meta+P': () => $project && goto(`/projects/${$project.id}/`),
'Meta+Shift+,': () => $project && goto(`/projects/${$project.id}/settings/`),
'Meta+R': () =>
$project && goto(`/projects/${$project.id}/player/${format(new Date(), 'yyyy-MM-dd')}`),
'a i p': () => $project && goto(`/projects/${$project.id}/aiplayground/`)
})
);
@ -76,12 +54,7 @@
>
<div class="relative">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<svg class="mr-2 h-5 w-5" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"
><path
d="M8 12a4 4 0 110-8 4 4 0 010 8zm9.707 4.293l-4.82-4.82A5.968 5.968 0 0014 8 6 6 0 002 8a6 6 0 006 6 5.968 5.968 0 003.473-1.113l4.82 4.82a.997.997 0 001.414 0 .999.999 0 000-1.414z"
fill="#5C5F62"
/></svg
>
<IconSearch class="h-5 w-5 text-zinc-500" />
</div>
<form
on:submit|preventDefault={onSearchSubmit}
@ -97,7 +70,6 @@
autocomplete="off"
aria-label="Search input"
class="block w-full min-w-0 flex-1 rounded border border-zinc-700 bg-zinc-800 p-[3px] px-2 pl-10 text-zinc-200 placeholder:text-zinc-500 sm:text-sm sm:leading-6"
style=""
/>
</form>
</div>
@ -126,25 +98,7 @@
class="block rounded p-1 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200"
href="/projects/{$project?.id}/settings"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<IconSettings class="h-6 w-6" />
</a>
</Tooltip>
</li>

View File

@ -1,27 +1,28 @@
import { building } from '$app/environment';
import type { Session } from '$lib/sessions';
import type { Status } from '$lib/git/statuses';
import type { Session, Delta, Status } from '$lib/api';
import { readable } from 'svelte/store';
import type { LayoutLoad } from './$types';
import type { Delta } from '$lib/deltas';
import type { Readable } from 'svelte/store';
export const prerender = false;
export const load: LayoutLoad = async ({ parent, params }) => {
const { projects } = await parent();
const sessions = building
? readable<Session[]>([])
: await import('$lib/sessions').then((m) => m.default({ projectId: params.projectId }));
: await import('$lib/api').then((m) => m.sessions.Sessions({ projectId: params.projectId }));
const statuses = building
? readable<Record<string, Status>>({})
: await import('$lib/git/statuses').then((m) => m.default({ projectId: params.projectId }));
: await import('$lib/api').then((m) =>
m.git.statuses.Statuses({ projectId: params.projectId })
);
const head = building
? readable<string>('')
: await import('$lib/git/head').then((m) => m.default({ projectId: params.projectId }));
: await import('$lib/api').then((m) => m.git.heads.Head({ projectId: params.projectId }));
const deltas = building
? () => Promise.resolve(readable<Record<string, Delta[]>>({}))
: (sessionId: string) =>
import('$lib/deltas').then((m) => m.default({ projectId: params.projectId, sessionId }));
import('$lib/api').then((m) => m.deltas.Deltas({ projectId: params.projectId, sessionId }));
const cache: Record<string, Promise<Readable<Record<string, Delta[]>>>> = {};
const cachedDeltas = (sessionId: string) => {

View File

@ -2,10 +2,9 @@
import { getTime, subDays } from 'date-fns';
import type { PageData } from './$types';
import { derived } from 'svelte/store';
import { IconGitBranch } from '$lib/components/icons';
import { IconGitBranch, IconLoading } from '$lib/components/icons';
import { asyncDerived } from '@square/svelte-store';
import type { Delta } from '$lib/deltas';
import IconRotateClockwise from '$lib/components/icons/IconRotateClockwise.svelte';
import type { Delta } from '$lib/api';
import FileSummaries from './FileSummaries.svelte';
import { Button, Statuses, Tooltip } from '$lib/components';
@ -124,7 +123,7 @@
<ul class="mr-1 flex flex-col space-y-4 overflow-y-auto pl-8 pr-5 pb-8">
{#await fileDeltas.load()}
<li>
<IconRotateClockwise class="animate-spin" />
<IconLoading class="animate-spin" />
</li>
{:then}
<FileSummaries projectId={$project?.id} fileDeltas={$fileDeltas} />

View File

@ -1,13 +1,16 @@
import { building } from '$app/environment';
import type { PageLoad } from './$types';
import { readable } from 'svelte/store';
import type { Activity } from '$lib/git/activity';
import type { Activity } from '$lib/api';
import { wrapLoadWithSentry } from '@sentry/sveltekit';
export const load: PageLoad = async ({ params }) => {
export const load: PageLoad = wrapLoadWithSentry(async ({ params }) => {
const activity = building
? readable<Activity[]>([])
: await import('$lib/git/activity').then((m) => m.default({ projectId: params.projectId }));
: await import('$lib/api').then((m) =>
m.git.activities.Activities({ projectId: params.projectId })
);
return {
activity
};
};
});

View File

@ -1,5 +1,5 @@
<script lang="ts">
import type { Delta } from '$lib/deltas';
import type { Delta } from '$lib/api';
import { bucketByTimestamp } from './histogram';
export let deltas: Delta[];

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { format, startOfDay } from 'date-fns';
import type { Delta } from '$lib/deltas';
import type { Delta } from '$lib/api';
import { derived, type Readable } from 'svelte/store';
import { collapsable } from '$lib/paths';
import FileActivity from './FileActivity.svelte';

View File

@ -3,8 +3,7 @@
import { Button } from '$lib/components';
import { collapsable } from '$lib/paths';
import { derived, writable } from 'svelte/store';
import * as git from '$lib/git';
import { Status } from '$lib/git/statuses';
import { git, Status } from '$lib/api';
import DiffViewer from '$lib/components/DiffViewer.svelte';
import { error, success } from '$lib/toasts';
import { fly } from 'svelte/transition';
@ -161,7 +160,7 @@
<Dialog bind:this={connectToCloudDialog}>
<svelte:fragment slot="title">GitButler Cloud required</svelte:fragment>
<svelte:fragment>
<div class="w-[640px]">
<p class="py-2">
By connecting to GitButler Cloud you'll unlock improved, cloud only features, including
AI-generated commit summaries, and the assurance of never losing your work with synced
@ -181,28 +180,22 @@
secure and easily recoverable.
</span>
</p>
</svelte:fragment>
<svelte:fragment slot="controls" let:hide>
<Button filled on:click={hide}>Cancel</Button>
<Button
filled
role="primary"
on:click={() => {
enableProjectSync();
hide();
}}>Connect</Button
>
</div>
<svelte:fragment slot="controls" let:close>
<Button filled={false} outlined={true} on:click={close}>Cancel</Button>
<Button role="primary" on:click={() => enableProjectSync().finally(close)}>Connect</Button>
</svelte:fragment>
</Dialog>
<div id="commit-page" class="flex h-full w-full">
<div class="commit-panel-container flex flex-col border-r border-zinc-700 p-4 pt-2">
<h1 class="pb-2 text-xl font-bold">Commit</h1>
<div class="commit-panel-container flex h-full flex-col border-r border-zinc-700 px-4">
<form
on:submit|preventDefault={onCommit}
class="flex h-full w-1/3 min-w-[500px] flex-col gap-4"
>
<ul class="flex h-full w-full flex-col rounded border border-gb-700 bg-card-default pb-1">
<h1 class="pt-2 text-xl font-bold">Commit</h1>
<ul
class="flex h-full w-full flex-col overflow-auto rounded border border-gb-700 bg-card-default"
>
<header class="flex w-full items-center rounded-tl rounded-tr bg-card-active p-2">
<input
type="checkbox"
@ -220,9 +213,9 @@
</h1>
</header>
<div class="changed-file-list-container overflow-y-auto">
<div class="changed-file-list-container h-100 overflow-y-auto">
{#each Object.entries($statuses) as [path, status]}
<li class="bg-card-default">
<li class="bg-card-default last:mb-1">
<div
class:bg-[#3356C2]={$selectedDiffPath === path}
class:hover:bg-divider={$selectedDiffPath !== path}
@ -260,73 +253,75 @@
</div>
</ul>
<input
name="summary"
class="
w-full rounded border border-zinc-600 bg-zinc-700 p-2 text-zinc-100
hover:border-zinc-500/80
focus:border-[1px] focus:focus:border-blue-600
focus:ring-2 focus:ring-blue-600/30
"
disabled={isGeneratingCommitMessage || isCommitting}
type="text"
placeholder="Summary (required)"
bind:value={summary}
required
/>
<div class="commit-description-container relative h-36">
{#if isGeneratingCommitMessage}
<div
in:fly={{ y: 8, duration: 500 }}
out:fly={{ y: -8, duration: 500 }}
class="generating-commit absolute top-0 right-0 bottom-0 left-0 rounded border-2 border-[#502E5C] "
>
<div
class="generating-commit-message absolute bottom-0 left-0 rounded-tr bg-[#782E94] bg-gradient-to-b from-[#623871] to-[#502E5C] py-1 px-2"
>
<span>✨ Summarizing changes</span>
<span class="dot-container">
<div class="dot" />
<div class="dot" />
<div class="dot" />
</span>
</div>
</div>
{/if}
<textarea
name="description"
disabled={isGeneratingCommitMessage || isCommitting}
<div class="bottom-controller-container flex flex-col gap-2 pb-4">
<input
name="summary"
class="
h-full w-full rounded border border-zinc-600 bg-zinc-700 p-2 text-zinc-100
w-full rounded border border-zinc-600 bg-zinc-700 p-2 text-zinc-100
hover:border-zinc-500/80
focus:border-[1px] focus:focus:border-blue-600
focus:ring-2 focus:ring-blue-600/30
"
rows="10"
placeholder="Description (optional)"
bind:value={description}
disabled={isGeneratingCommitMessage || isCommitting}
type="text"
placeholder="Summary (required)"
bind:value={summary}
required
/>
</div>
<div class="flex justify-between">
<Button
role="purple"
disabled={!isGenerateCommitEnabled}
on:click={onGenerateCommitMessage}
loading={isGeneratingCommitMessage}
>
✨ Autowrite
</Button>
<div class="commit-description-container relative h-36">
{#if isGeneratingCommitMessage}
<div
in:fly={{ y: 8, duration: 500 }}
out:fly={{ y: -8, duration: 500 }}
class="generating-commit absolute top-0 right-0 bottom-0 left-0 rounded border-2 border-[#502E5C] "
>
<div
class="generating-commit-message absolute bottom-0 left-0 rounded-tr bg-[#782E94] bg-gradient-to-b from-[#623871] to-[#502E5C] py-1 px-2"
>
<span>✨ Summarizing changes</span>
<span class="dot-container">
<div class="dot" />
<div class="dot" />
<div class="dot" />
</span>
</div>
</div>
{/if}
<textarea
name="description"
disabled={isGeneratingCommitMessage || isCommitting}
class="
h-full w-full resize-none rounded border border-zinc-600 bg-zinc-700 p-2 text-zinc-100
hover:border-zinc-500/80
focus:border-[1px] focus:focus:border-blue-600
focus:ring-2 focus:ring-blue-600/30
"
rows="10"
placeholder="Description (optional)"
bind:value={description}
/>
</div>
<Button
loading={isCommitting}
disabled={!isCommitEnabled || isGeneratingCommitMessage}
role="primary"
type="submit"
>
Commit changes
</Button>
<div class="flex justify-between">
<Button
role="purple"
disabled={!isGenerateCommitEnabled}
on:click={onGenerateCommitMessage}
loading={isGeneratingCommitMessage}
>
✨ Autowrite
</Button>
<Button
loading={isCommitting}
disabled={!isCommitEnabled || isGeneratingCommitMessage}
role="primary"
type="submit"
>
Commit changes
</Button>
</div>
</div>
</form>
</div>

View File

@ -1,14 +1,15 @@
import { building } from '$app/environment';
import { readable } from 'svelte/store';
import type { PageLoad } from '../$types';
import { wrapLoadWithSentry } from '@sentry/sveltekit';
export const load: PageLoad = async ({ parent, params }) => {
export const load: PageLoad = wrapLoadWithSentry(async ({ parent, params }) => {
const { project } = await parent();
const diffs = building
? readable<Record<string, string>>({})
: await import('$lib/git/diffs').then((m) => m.default({ projectId: params.projectId }));
: await import('$lib/api').then((m) => m.git.diffs.Diffs({ projectId: params.projectId }));
return {
diffs,
project
};
};
});

View File

@ -0,0 +1,15 @@
import { redirect } from '@sveltejs/kit';
import { format } from 'date-fns';
import { get } from 'svelte/store';
import type { PageLoad } from './$types';
import { wrapLoadWithSentry } from '@sentry/sveltekit';
export const load: PageLoad = wrapLoadWithSentry(async ({ parent, url }) => {
const { sessions, projectId } = await parent();
const date = format(new Date(), 'yyyy-MM-dd');
const dateSessions = get(sessions).filter(
(session) => format(session.meta.startTimestampMs, 'yyyy-MM-dd') === date
);
const firstSession = dateSessions[dateSessions.length - 1];
throw redirect(302, `/projects/${projectId}/player/${date}/${firstSession.id}${url.search}`);
});

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { list as listDeltas } from '$lib/deltas';
import { deltas } from '$lib/api';
import { asyncDerived } from '@square/svelte-store';
import { format } from 'date-fns';
import { derived } from 'svelte/store';
@ -14,7 +14,7 @@
const dates = asyncDerived([sessions, fileFilter], async ([sessions, fileFilter]) => {
const sessionDeltas = await Promise.all(
sessions.map((session) =>
listDeltas({
deltas.list({
projectId,
sessionId: session.id,
paths: fileFilter ? [fileFilter] : undefined

View File

@ -2,8 +2,9 @@ import { redirect } from '@sveltejs/kit';
import { format } from 'date-fns';
import { get } from 'svelte/store';
import type { PageLoad } from './$types';
import { wrapLoadWithSentry } from '@sentry/sveltekit';
export const load: PageLoad = async ({ parent, params, url }) => {
export const load: PageLoad = wrapLoadWithSentry(async ({ parent, params, url }) => {
const { sessions, projectId } = await parent();
const dateSessions = get(sessions).filter(
(session) => format(session.meta.startTimestampMs, 'yyyy-MM-dd') === params.date
@ -13,4 +14,4 @@ export const load: PageLoad = async ({ parent, params, url }) => {
302,
`/projects/${projectId}/player/${params.date}/${firstSession.id}${url.search}`
);
};
});

View File

@ -1,20 +1,21 @@
<script lang="ts" context="module">
import { listFiles, type Session } from '$lib/sessions';
import { list as listDeltas, type Delta } from '$lib/deltas';
import { deltas, files, type Session, type Delta } from '$lib/api';
const enrichSession = async (projectId: string, session: Session, paths?: string[]) => {
const files = await listFiles({ projectId, sessionId: session.id, paths });
const deltas = await listDeltas({ projectId, sessionId: session.id, paths }).then((deltas) =>
Object.entries(deltas)
.flatMap(([path, deltas]) => deltas.map((delta) => [path, delta] as [string, Delta]))
.sort((a, b) => a[1].timestampMs - b[1].timestampMs)
);
const deltasFiles = new Set(deltas.map(([path]) => path));
const sessionFiles = await files.list({ projectId, sessionId: session.id, paths });
const sessionDeltas = await deltas
.list({ projectId, sessionId: session.id, paths })
.then((deltas) =>
Object.entries(deltas)
.flatMap(([path, deltas]) => deltas.map((delta) => [path, delta] as [string, Delta]))
.sort((a, b) => a[1].timestampMs - b[1].timestampMs)
);
const deltasFiles = new Set(sessionDeltas.map(([path]) => path));
return {
...session,
files: Object.fromEntries(
Object.entries(files).filter(([filepath]) => deltasFiles.has(filepath))
Object.entries(sessionFiles).filter(([filepath]) => deltasFiles.has(filepath))
),
deltas
deltas: sessionDeltas
};
};
</script>

View File

@ -1,7 +1,6 @@
<script lang="ts">
import type { Delta } from '$lib/deltas';
import type { Delta } from '$lib/api';
import slider from '$lib/slider';
import { onMount } from 'svelte';
type RichSession = { id: string; deltas: [string, Delta][] };
export let sessions: RichSession[];

View File

@ -1,12 +1,10 @@
<script lang="ts">
import type { PageData } from './$types';
import { search, type SearchResult } from '$lib';
import { IconChevronLeft, IconChevronRight } from '$lib/components/icons';
import { listFiles } from '$lib/sessions';
import { files, deltas, searchResults, type SearchResult } from '$lib/api';
import { asyncDerived } from '@square/svelte-store';
import { IconRotateClockwise } from '$lib/components/icons';
import { IconLoading } from '$lib/components/icons';
import { format, formatDistanceToNow } from 'date-fns';
import { list as listDeltas } from '$lib/deltas';
import { CodeViewer } from '$lib/components';
import { page } from '$app/stores';
import { derived } from 'svelte/store';
@ -29,16 +27,17 @@
index,
highlighted
}: SearchResult) => {
const [doc, deltas] = await Promise.all([
listFiles({ projectId, sessionId, paths: [filePath] }).then((r) => r[filePath] ?? ''),
listDeltas({ projectId, sessionId, paths: [filePath] })
const [doc, dd] = await Promise.all([
files.list({ projectId, sessionId, paths: [filePath] }).then((r) => r[filePath] ?? ''),
deltas
.list({ projectId, sessionId, paths: [filePath] })
.then((r) => r[filePath] ?? [])
.then((d) => d.slice(0, index + 1))
]);
const date = format(deltas[deltas.length - 1].timestampMs, 'yyyy-MM-dd');
const date = format(dd[dd.length - 1].timestampMs, 'yyyy-MM-dd');
return {
doc,
deltas,
deltas: dd,
filepath: filePath,
highlight: highlighted,
sessionId,
@ -47,11 +46,11 @@
};
};
const { store: searchResults, state: searchState } = asyncDerived(
const { store: results, state: searchState } = asyncDerived(
[query, project, offset],
async ([query, project, offset]) => {
if (!query || !project) return { page: [], total: 0, haveNext: false, havePrev: false };
const results = await search({ projectId: project.id, query, limit, offset });
const results = await searchResults.list({ projectId: project.id, query, limit, offset });
return {
page: await Promise.all(results.page.map(fetchResultData)),
haveNext: offset + limit < results.total,
@ -70,7 +69,7 @@
</figcaption>
<div class="mx-auto">
<IconRotateClockwise class="h-20 w-20 animate-spin" />
<IconLoading class="h-20 w-20 animate-spin" />
</div>
{:else if $searchState?.isError}
<figcaption>
@ -78,16 +77,16 @@
</figcaption>
{:else if $searchState?.isLoaded}
<figcaption class="mt-14">
{#if $searchResults.total > 0}
{#if $results.total > 0}
<p class="mb-2 text-xl text-[#D4D4D8]">Results for "{$query}"</p>
<p class="text-lg text-[#717179]">{$searchResults.total} change instances</p>
<p class="text-lg text-[#717179]">{$results.total} change instances</p>
{:else}
<p class="mb-2 text-xl text-[#D4D4D8]">No results for "{$query}"</p>
{/if}
</figcaption>
<ul class="search-result-list -mr-14 flex flex-auto flex-col gap-6 overflow-auto pb-6">
{#each $searchResults.page as { doc, deltas, filepath, highlight, sessionId, projectId, date }}
{#each $results.page as { doc, deltas, filepath, highlight, sessionId, projectId, date }}
{@const timestamp = deltas[deltas.length - 1].timestampMs}
<li class="search-result mr-14">
<a
@ -111,18 +110,18 @@
<nav class="mx-auto flex text-zinc-400">
<button
on:click={openPrevPage}
disabled={!$searchResults.havePrev}
disabled={!$results.havePrev}
title="Back"
class:text-zinc-50={$searchResults.havePrev}
class:text-zinc-50={$results.havePrev}
class="h-9 w-9 rounded-tl-md rounded-bl-md border border-r-0 border-zinc-700 hover:bg-zinc-700"
>
<IconChevronLeft class="ml-1 h-5 w-6" />
</button>
<button
on:click={openNextPage}
disabled={!$searchResults.haveNext}
disabled={!$results.haveNext}
title="Next"
class:text-zinc-50={$searchResults.haveNext}
class:text-zinc-50={$results.haveNext}
class="h-9 w-9 rounded-tr-md rounded-br-md border border-l border-zinc-700 hover:bg-zinc-700"
>
<IconChevronRight class="ml-1 h-5 w-6" />

View File

@ -3,7 +3,7 @@
import ResizeObserver from 'svelte-resize-observer';
import setupTerminal from './terminal';
import 'xterm/css/xterm.css';
import type { Project } from '$lib/projects';
import type { Project } from '$lib/api';
import { debounce } from '$lib/utils';
import { Button, Statuses } from '$lib/components';

View File

@ -1,4 +1,4 @@
import type { Project } from '$lib/projects';
import type { Project } from '$lib/api';
import { Terminal } from 'xterm';
import { CanvasAddon } from 'xterm-addon-canvas';
import { WebglAddon } from 'xterm-addon-webgl';

View File

@ -6,6 +6,9 @@ const config = {
preprocess: vitePreprocess(),
kit: {
adapter: staticAdapter({
pages: 'build',
assets: 'build',
fallback: 'index.html',
precompress: true,
strict: false
})

View File

@ -1,8 +1,28 @@
import { defineConfig } from 'vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { sentrySvelteKit } from '@sentry/sveltekit';
export default defineConfig({
plugins: [sveltekit()],
plugins: [
sentrySvelteKit({
sourceMapsUploadOptions: {
org: 'gitbutler',
project: 'desktop',
// this is nikita galaiko's personal sentry api token.
authToken: '04c6bc1df15346f39ed2fbeb99c0a8e25bcbedc4aba9461bb3a471733b8c80db',
include: ['build'],
cleanArtifacts: true,
setCommits: {
auto: true,
ignoreMissing: true,
ignoreEmpty: true
},
telemetry: false,
uploadSourceMaps: process.env.SENTRY_RELEASE !== undefined
}
}),
sveltekit()
],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// prevent vite from obscuring rust errors