Add Rig native model providers
This commit is contained in:
Generated
+385
-8
@@ -1495,6 +1495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1"
|
checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-sys",
|
"aws-lc-sys",
|
||||||
|
"untrusted 0.7.1",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5986,6 +5987,7 @@ dependencies = [
|
|||||||
"galaxy_agent_core",
|
"galaxy_agent_core",
|
||||||
"rig-bedrock",
|
"rig-bedrock",
|
||||||
"rig-core",
|
"rig-core",
|
||||||
|
"rig-vertexai",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -7230,6 +7232,241 @@ dependencies = [
|
|||||||
"gl_generator",
|
"gl_generator",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-cloud-aiplatform-v1"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c4b46c4e50f6a09b85dae39be560c263a20ec06c2b0d5a96d65423936bf238f"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"google-cloud-api",
|
||||||
|
"google-cloud-gax",
|
||||||
|
"google-cloud-gax-internal",
|
||||||
|
"google-cloud-iam-v1",
|
||||||
|
"google-cloud-location",
|
||||||
|
"google-cloud-longrunning",
|
||||||
|
"google-cloud-lro",
|
||||||
|
"google-cloud-rpc",
|
||||||
|
"google-cloud-type",
|
||||||
|
"google-cloud-wkt",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with 3.21.0",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-cloud-api"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "19dd5722ba4d24fbc19f6a44b88c335852c9a98d058bc0d6073c9a730c026cad"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"google-cloud-wkt",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with 3.21.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-cloud-auth"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f54aab44c16b8463ae11b165a87c3d484780231f157bb1ed65843d591beb5abd"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"aws-lc-rs",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"chrono",
|
||||||
|
"google-cloud-gax",
|
||||||
|
"hex",
|
||||||
|
"hmac 0.13.0",
|
||||||
|
"http 1.5.0",
|
||||||
|
"jsonwebtoken",
|
||||||
|
"reqwest 0.13.4",
|
||||||
|
"rustc_version",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2 0.11.0",
|
||||||
|
"thiserror 2.0.19",
|
||||||
|
"time",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-cloud-gax"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9a46dd0fd026bbc4a5d84e6ab0c941cee6e3b057976a0bb107fdb5238ce598f"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures",
|
||||||
|
"google-cloud-rpc",
|
||||||
|
"google-cloud-wkt",
|
||||||
|
"http 1.5.0",
|
||||||
|
"pin-project",
|
||||||
|
"rand 0.10.2",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.19",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-cloud-gax-internal"
|
||||||
|
version = "0.7.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb04c54317ace06d489213f761797240b3046142a9b7ce6b9a82a9d134e193d1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures",
|
||||||
|
"google-cloud-auth",
|
||||||
|
"google-cloud-gax",
|
||||||
|
"google-cloud-rpc",
|
||||||
|
"google-cloud-wkt",
|
||||||
|
"h2",
|
||||||
|
"http 1.5.0",
|
||||||
|
"http-body 1.1.0",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"lazy_static",
|
||||||
|
"opentelemetry",
|
||||||
|
"opentelemetry-semantic-conventions",
|
||||||
|
"opentelemetry_sdk",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project",
|
||||||
|
"prost",
|
||||||
|
"prost-types",
|
||||||
|
"reqwest 0.13.4",
|
||||||
|
"rustc_version",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.19",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tonic",
|
||||||
|
"tonic-prost",
|
||||||
|
"tower",
|
||||||
|
"tracing",
|
||||||
|
"tracing-opentelemetry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-cloud-iam-v1"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34cdf5acc7ef946ee2db7a7f62bd436d8395a6543b4beef110cdc061fcf578bb"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"google-cloud-gax",
|
||||||
|
"google-cloud-gax-internal",
|
||||||
|
"google-cloud-type",
|
||||||
|
"google-cloud-wkt",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with 3.21.0",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-cloud-location"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "280d5acdba8fcb1232c0719ed788d85b7e362b82cbb425b7050d3ce46f075ede"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"google-cloud-gax",
|
||||||
|
"google-cloud-gax-internal",
|
||||||
|
"google-cloud-wkt",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with 3.21.0",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-cloud-longrunning"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e6ce05df0aea2c08472983ce2bbbed9483cbb637b89ff69a7c4ef94371fe4f2"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"google-cloud-gax",
|
||||||
|
"google-cloud-gax-internal",
|
||||||
|
"google-cloud-rpc",
|
||||||
|
"google-cloud-wkt",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with 3.21.0",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-cloud-lro"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd7cca2b991d619525d72a170ca7f413cb520872702442da22ac9af650a8e786"
|
||||||
|
dependencies = [
|
||||||
|
"google-cloud-gax",
|
||||||
|
"google-cloud-gax-internal",
|
||||||
|
"google-cloud-longrunning",
|
||||||
|
"google-cloud-rpc",
|
||||||
|
"google-cloud-wkt",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-cloud-rpc"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2162c08a89118130979ba261080e960e44cdcb2d6e2ab8ca9b1da245285d353"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"google-cloud-wkt",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with 3.21.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-cloud-type"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63acc3a92a85f96bab021c3a3e29b53bbacc97651e1b524d4c2991960a63eb82"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"google-cloud-wkt",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with 3.21.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-cloud-wkt"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fccf98cfd5481a5f5a285181ab0c62123d7d47cd2bb7299448440649349e4e7"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with 3.21.0",
|
||||||
|
"thiserror 2.0.19",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpu-allocator"
|
name = "gpu-allocator"
|
||||||
version = "0.28.0"
|
version = "0.28.0"
|
||||||
@@ -7816,6 +8053,19 @@ dependencies = [
|
|||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-timeout"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
|
||||||
|
dependencies = [
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-tls"
|
name = "hyper-tls"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -8745,6 +8995,22 @@ dependencies = [
|
|||||||
"uuid-simd",
|
"uuid-simd",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonwebtoken"
|
||||||
|
version = "10.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-rs",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"signature",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kasuari"
|
name = "kasuari"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
@@ -10878,6 +11144,42 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry"
|
||||||
|
version = "0.32.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"js-sys",
|
||||||
|
"pin-project-lite",
|
||||||
|
"thiserror 2.0.19",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry-semantic-conventions"
|
||||||
|
version = "0.32.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c913ac17a6c451661ee255f4625d143e51647ae78ebd969b75e41c4442f4fe47"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry_sdk"
|
||||||
|
version = "0.32.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-util",
|
||||||
|
"opentelemetry",
|
||||||
|
"percent-encoding",
|
||||||
|
"portable-atomic",
|
||||||
|
"rand 0.9.5",
|
||||||
|
"thiserror 2.0.19",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -12876,14 +13178,14 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rig-bedrock"
|
name = "rig-bedrock"
|
||||||
version = "0.41.0"
|
version = "0.41.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/0xPlaygrounds/rig.git?rev=1f9547774edb4c269be991ac42eb043fd7b6e87f#1f9547774edb4c269be991ac42eb043fd7b6e87f"
|
||||||
checksum = "9a40dec6d2833da0f482f97f105b846b46b82b8ab904b78f2365e263e016f09d"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"aws-config",
|
"aws-config",
|
||||||
"aws-sdk-bedrockruntime",
|
"aws-sdk-bedrockruntime",
|
||||||
"aws-smithy-types",
|
"aws-smithy-types",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
"futures",
|
||||||
"rig-core",
|
"rig-core",
|
||||||
"rig-derive",
|
"rig-derive",
|
||||||
"schemars 1.2.2",
|
"schemars 1.2.2",
|
||||||
@@ -12898,8 +13200,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rig-core"
|
name = "rig-core"
|
||||||
version = "0.41.0"
|
version = "0.41.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/0xPlaygrounds/rig.git?rev=1f9547774edb4c269be991ac42eb043fd7b6e87f#1f9547774edb4c269be991ac42eb043fd7b6e87f"
|
||||||
checksum = "35f5520515ae8f6851adcbc6fde9eea8e96f657418c062e16c82cd81cce44e8e"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as-any",
|
"as-any",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@@ -12932,8 +13233,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rig-derive"
|
name = "rig-derive"
|
||||||
version = "0.41.0"
|
version = "0.41.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/0xPlaygrounds/rig.git?rev=1f9547774edb4c269be991ac42eb043fd7b6e87f#1f9547774edb4c269be991ac42eb043fd7b6e87f"
|
||||||
checksum = "eb868fcebdf3ba425e3afad2e4926bb6d9e1188a856843b00bcee2e15c07424f"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case 0.11.0",
|
"convert_case 0.11.0",
|
||||||
"proc-macro-crate 3.5.0",
|
"proc-macro-crate 3.5.0",
|
||||||
@@ -12942,6 +13242,22 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rig-vertexai"
|
||||||
|
version = "0.41.0"
|
||||||
|
source = "git+https://github.com/0xPlaygrounds/rig.git?rev=1f9547774edb4c269be991ac42eb043fd7b6e87f#1f9547774edb4c269be991ac42eb043fd7b6e87f"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"google-cloud-aiplatform-v1",
|
||||||
|
"google-cloud-auth",
|
||||||
|
"rig-core",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.19",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -12952,7 +13268,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
"libc",
|
"libc",
|
||||||
"untrusted",
|
"untrusted 0.9.0",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -13401,7 +13717,7 @@ dependencies = [
|
|||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"untrusted",
|
"untrusted 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -15617,6 +15933,44 @@ version = "1.1.2+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tonic"
|
||||||
|
version = "0.14.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"http 1.5.0",
|
||||||
|
"http-body 1.1.0",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-timeout",
|
||||||
|
"hyper-util",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project",
|
||||||
|
"rustls-native-certs",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tokio-stream",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tonic-prost"
|
||||||
|
version = "0.14.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"prost",
|
||||||
|
"tonic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -15625,9 +15979,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"indexmap 2.14.0",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -15730,6 +16087,20 @@ dependencies = [
|
|||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-opentelemetry"
|
||||||
|
version = "0.33.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"opentelemetry",
|
||||||
|
"tracing",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-subscriber"
|
name = "tracing-subscriber"
|
||||||
version = "0.3.23"
|
version = "0.3.23"
|
||||||
@@ -16127,6 +16498,12 @@ version = "0.2.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|||||||
+3
-2
@@ -262,8 +262,9 @@ reqwest = { version = "0.13", features = [
|
|||||||
"stream",
|
"stream",
|
||||||
] }
|
] }
|
||||||
reqwest-eventsource = { package = "aha-reqwest-eventsource", version = "0.1" }
|
reqwest-eventsource = { package = "aha-reqwest-eventsource", version = "0.1" }
|
||||||
rig-core = "=0.41.0"
|
rig-core = { git = "https://github.com/0xPlaygrounds/rig.git", rev = "1f9547774edb4c269be991ac42eb043fd7b6e87f", package = "rig-core" }
|
||||||
rig-bedrock = "=0.41.0"
|
rig-bedrock = { git = "https://github.com/0xPlaygrounds/rig.git", rev = "1f9547774edb4c269be991ac42eb043fd7b6e87f", package = "rig-bedrock" }
|
||||||
|
rig-vertexai = { git = "https://github.com/0xPlaygrounds/rig.git", rev = "1f9547774edb4c269be991ac42eb043fd7b6e87f", package = "rig-vertexai" }
|
||||||
resvg = "0.47.0"
|
resvg = "0.47.0"
|
||||||
rust-embed = { version = "8.7.0", features = ["include-exclude"] }
|
rust-embed = { version = "8.7.0", features = ["include-exclude"] }
|
||||||
rustc-hash = "2.1.1"
|
rustc-hash = "2.1.1"
|
||||||
|
|||||||
@@ -684,9 +684,11 @@ impl ShellCommandExecutor {
|
|||||||
.force_refresh_senders
|
.force_refresh_senders
|
||||||
.keys()
|
.keys()
|
||||||
.find(|selector| {
|
.find(|selector| {
|
||||||
selector
|
selector.get_block(&terminal_model).is_some_and(|block| {
|
||||||
.get_block(&terminal_model)
|
block.id() == block_id
|
||||||
.is_some_and(|block| block.id() == block_id)
|
&& block.is_active_and_long_running()
|
||||||
|
&& !block.finished()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.cloned();
|
.cloned();
|
||||||
drop(terminal_model);
|
drop(terminal_model);
|
||||||
|
|||||||
@@ -103,11 +103,14 @@ fn force_refresh_block_reports_and_resolves_matching_poll() {
|
|||||||
ActiveSession::new(sessions.clone(), model_event_dispatcher.clone(), ctx)
|
ActiveSession::new(sessions.clone(), model_event_dispatcher.clone(), ctx)
|
||||||
});
|
});
|
||||||
let terminal_model = Arc::new(FairMutex::new(TerminalModel::mock(None, None)));
|
let terminal_model = Arc::new(FairMutex::new(TerminalModel::mock(None, None)));
|
||||||
|
terminal_model
|
||||||
|
.lock()
|
||||||
|
.simulate_long_running_block("sleep 120", "still running");
|
||||||
let block_id = terminal_model.lock().active_block_id().clone();
|
let block_id = terminal_model.lock().active_block_id().clone();
|
||||||
let executor = app.add_model(|ctx| {
|
let executor = app.add_model(|ctx| {
|
||||||
ShellCommandExecutor::new(
|
ShellCommandExecutor::new(
|
||||||
active_session,
|
active_session,
|
||||||
terminal_model,
|
terminal_model.clone(),
|
||||||
&model_event_dispatcher,
|
&model_event_dispatcher,
|
||||||
terminal_view_id,
|
terminal_view_id,
|
||||||
ctx,
|
ctx,
|
||||||
@@ -124,6 +127,17 @@ fn force_refresh_block_reports_and_resolves_matching_poll() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert!(matches!(rx.try_recv(), Ok(Some(()))));
|
assert!(matches!(rx.try_recv(), Ok(Some(()))));
|
||||||
|
|
||||||
|
let (tx, _rx) = oneshot::channel();
|
||||||
|
executor.update(&mut app, |executor, _| {
|
||||||
|
executor
|
||||||
|
.force_refresh_senders
|
||||||
|
.insert(BlockSelector::Id(block_id.clone()), tx);
|
||||||
|
});
|
||||||
|
terminal_model.lock().finish_block();
|
||||||
|
assert!(executor.update(&mut app, |executor, _| {
|
||||||
|
!executor.force_refresh_block(&block_id)
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -265,6 +265,8 @@ impl CLISubagentController {
|
|||||||
let block_id = block.id().clone();
|
let block_id = block.id().clone();
|
||||||
let conversation_id = block.ai_conversation_id();
|
let conversation_id = block.ai_conversation_id();
|
||||||
let requested_command_action_id = block.requested_command_action_id().cloned();
|
let requested_command_action_id = block.requested_command_action_id().cloned();
|
||||||
|
let should_skip_completion_assessment =
|
||||||
|
!should_request_completion_assessment(block.long_running_control_state());
|
||||||
let completion = match (&block_completed_event.block_type, conversation_id) {
|
let completion = match (&block_completed_event.block_type, conversation_id) {
|
||||||
(BlockType::User(completed), Some(conversation_id)) => {
|
(BlockType::User(completed), Some(conversation_id)) => {
|
||||||
let command = if completed.command_with_obfuscated_secrets.is_empty() {
|
let command = if completed.command_with_obfuscated_secrets.is_empty() {
|
||||||
@@ -310,17 +312,49 @@ impl CLISubagentController {
|
|||||||
};
|
};
|
||||||
drop(terminal_model);
|
drop(terminal_model);
|
||||||
|
|
||||||
let Some(subagent_state) = me.active_subagents_by_block.get_mut(&block_id) else {
|
let Some(has_last_snapshot) = me
|
||||||
|
.active_subagents_by_block
|
||||||
|
.get(&block_id)
|
||||||
|
.map(|state| state.last_snapshot_at.is_some())
|
||||||
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if subagent_state.last_snapshot_at.is_some() {
|
if has_last_snapshot {
|
||||||
ctx.emit(CLISubagentEvent::UpdatedLastSnapshot);
|
ctx.emit(CLISubagentEvent::UpdatedLastSnapshot);
|
||||||
}
|
}
|
||||||
subagent_state.completion = completion;
|
|
||||||
if subagent_state.completion.is_none() {
|
// A Stop takeover intentionally cancels the subagent. The command may still
|
||||||
|
// finish later, but that completion must not start a new assessment turn. Also
|
||||||
|
// clean up the in-memory monitor state so the stopped subagent cannot linger in
|
||||||
|
// the UI or intercept later refreshes.
|
||||||
|
if should_skip_completion_assessment {
|
||||||
|
me.finish_subagent(
|
||||||
|
&block_id,
|
||||||
|
conversation_id,
|
||||||
|
requested_command_action_id,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_completion = {
|
||||||
|
let Some(subagent_state) = me.active_subagents_by_block.get_mut(&block_id)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
subagent_state.completion = completion;
|
||||||
|
subagent_state.completion.is_some()
|
||||||
|
};
|
||||||
|
if !has_completion {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"CLI monitor block {block_id:?} completed without final command metadata"
|
"CLI monitor block {block_id:?} completed without final command metadata"
|
||||||
);
|
);
|
||||||
|
me.finish_subagent(
|
||||||
|
&block_id,
|
||||||
|
conversation_id,
|
||||||
|
requested_command_action_id,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
me.advance_completed_subagent(&block_id, ctx);
|
me.advance_completed_subagent(&block_id, ctx);
|
||||||
@@ -380,7 +414,12 @@ impl CLISubagentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if completion.final_turn_started {
|
if completion.final_turn_started {
|
||||||
self.finish_completed_subagent(block_id, ctx);
|
self.finish_subagent(
|
||||||
|
block_id,
|
||||||
|
Some(completion.conversation_id),
|
||||||
|
completion.initial_requested_command_action_id,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,38 +443,55 @@ impl CLISubagentController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_completed_subagent(&mut self, block_id: &BlockId, ctx: &mut ModelContext<Self>) {
|
fn finish_subagent(
|
||||||
|
&mut self,
|
||||||
|
block_id: &BlockId,
|
||||||
|
conversation_id: Option<AIConversationId>,
|
||||||
|
initial_requested_command_action_id: Option<AIAgentActionId>,
|
||||||
|
ctx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
let Some(state) = self.active_subagents_by_block.remove(block_id) else {
|
let Some(state) = self.active_subagents_by_block.remove(block_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(completion) = state.completion else {
|
let conversation_id = conversation_id.or_else(|| {
|
||||||
return;
|
state
|
||||||
};
|
.completion
|
||||||
|
.as_ref()
|
||||||
|
.map(|completion| completion.conversation_id)
|
||||||
|
});
|
||||||
|
let initial_requested_command_action_id = initial_requested_command_action_id
|
||||||
|
.or_else(|| {
|
||||||
|
state
|
||||||
|
.completion
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|completion| completion.initial_requested_command_action_id.clone())
|
||||||
|
})
|
||||||
|
.or(state.initial_requested_command_action_id);
|
||||||
|
|
||||||
let deactivate_result =
|
if let Some(conversation_id) = conversation_id {
|
||||||
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, _| {
|
let deactivate_result =
|
||||||
history_model.deactivate_cli_subagent_task_for_conversation(
|
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, _| {
|
||||||
block_id,
|
history_model
|
||||||
completion.conversation_id,
|
.deactivate_cli_subagent_task_for_conversation(block_id, conversation_id)
|
||||||
)
|
});
|
||||||
});
|
if let Err(error) = deactivate_result {
|
||||||
if let Err(error) = deactivate_result {
|
log::error!("Failed to deactivate CLI monitor for block {block_id:?}: {error:?}");
|
||||||
log::error!(
|
}
|
||||||
"Failed to deactivate completed CLI monitor for block {block_id:?}: {error:?}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.emit(CLISubagentEvent::FinishedSubagent {
|
ctx.emit(CLISubagentEvent::FinishedSubagent {
|
||||||
block_id: block_id.clone(),
|
block_id: block_id.clone(),
|
||||||
conversation_id: Some(completion.conversation_id),
|
conversation_id,
|
||||||
initial_requested_command_action_id: completion.initial_requested_command_action_id,
|
initial_requested_command_action_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(agent_view_controller) = &self.agent_view_controller {
|
if let (Some(agent_view_controller), Some(conversation_id)) =
|
||||||
|
(&self.agent_view_controller, conversation_id)
|
||||||
|
{
|
||||||
agent_view_controller.update(ctx, |controller, ctx| {
|
agent_view_controller.update(ctx, |controller, ctx| {
|
||||||
let is_this_inline_conversation = controller.is_inline()
|
let is_this_inline_conversation = controller.is_inline()
|
||||||
&& controller.agent_view_state().active_conversation_id()
|
&& controller.agent_view_state().active_conversation_id()
|
||||||
== Some(completion.conversation_id);
|
== Some(conversation_id);
|
||||||
if is_this_inline_conversation {
|
if is_this_inline_conversation {
|
||||||
controller.exit_agent_view(ctx);
|
controller.exit_agent_view(ctx);
|
||||||
}
|
}
|
||||||
@@ -919,3 +975,42 @@ fn command_finished_block_id(result: &AIAgentActionResultType) -> Option<&BlockI
|
|||||||
| AIAgentActionResultType::WaitForEvents(_) => None,
|
| AIAgentActionResultType::WaitForEvents(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_request_completion_assessment(
|
||||||
|
control_state: Option<&LongRunningCommandControlState>,
|
||||||
|
) -> bool {
|
||||||
|
!control_state
|
||||||
|
.and_then(LongRunningCommandControlState::user_take_over_reason)
|
||||||
|
.is_some_and(UserTakeOverReason::is_stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stop_takeover_does_not_request_a_completion_assessment() {
|
||||||
|
let state = LongRunningCommandControlState::User {
|
||||||
|
reason: UserTakeOverReason::Stop,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(!should_request_completion_assessment(Some(&state)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_stop_control_states_can_request_a_completion_assessment() {
|
||||||
|
let agent_state = LongRunningCommandControlState::Agent {
|
||||||
|
is_blocked: false,
|
||||||
|
should_hide_responses: false,
|
||||||
|
};
|
||||||
|
let transfer_state = LongRunningCommandControlState::User {
|
||||||
|
reason: UserTakeOverReason::TransferFromAgent {
|
||||||
|
reason: "needs user input".to_owned(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(should_request_completion_assessment(None));
|
||||||
|
assert!(should_request_completion_assessment(Some(&agent_state)));
|
||||||
|
assert!(should_request_completion_assessment(Some(&transfer_state)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -230,6 +230,8 @@ impl ResponseStream {
|
|||||||
kind: client_config.kind,
|
kind: client_config.kind,
|
||||||
base_url: client_config.base_url.clone(),
|
base_url: client_config.base_url.clone(),
|
||||||
api_key: client_config.api_key.clone(),
|
api_key: client_config.api_key.clone(),
|
||||||
|
project_id: client_config.project_id.clone(),
|
||||||
|
location: client_config.location.clone(),
|
||||||
model: client_config
|
model: client_config
|
||||||
.model
|
.model
|
||||||
.clone()
|
.clone()
|
||||||
|
|||||||
@@ -167,6 +167,8 @@ impl CrosscheckReviewer {
|
|||||||
kind: client_config.kind,
|
kind: client_config.kind,
|
||||||
base_url: client_config.base_url.clone(),
|
base_url: client_config.base_url.clone(),
|
||||||
api_key: client_config.api_key.clone(),
|
api_key: client_config.api_key.clone(),
|
||||||
|
project_id: client_config.project_id.clone(),
|
||||||
|
location: client_config.location.clone(),
|
||||||
model: client_config
|
model: client_config
|
||||||
.model
|
.model
|
||||||
.clone()
|
.clone()
|
||||||
|
|||||||
+105
-8
@@ -5,6 +5,11 @@ use std::sync::{Arc, OnceLock};
|
|||||||
|
|
||||||
use ai::api_keys::ApiKeyManager;
|
use ai::api_keys::ApiKeyManager;
|
||||||
pub use ai::LLMId;
|
pub use ai::LLMId;
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
use galaxy_agent_rig::{
|
||||||
|
discover_anthropic_models, discover_gemini_models, validate_vertex_ai_credentials,
|
||||||
|
vertex_ai_model_catalog, RigModelInfo,
|
||||||
|
};
|
||||||
use galaxy_core::features::FeatureFlag;
|
use galaxy_core::features::FeatureFlag;
|
||||||
use galaxy_core::ui::icons::Icon;
|
use galaxy_core::ui::icons::Icon;
|
||||||
use galaxy_core::user_preferences::GetUserPreferences;
|
use galaxy_core::user_preferences::GetUserPreferences;
|
||||||
@@ -1005,6 +1010,8 @@ impl LLMPreferences {
|
|||||||
bool,
|
bool,
|
||||||
String,
|
String,
|
||||||
Option<String>,
|
Option<String>,
|
||||||
|
Option<String>,
|
||||||
|
Option<String>,
|
||||||
Vec<OpenAIModelConfig>,
|
Vec<OpenAIModelConfig>,
|
||||||
);
|
);
|
||||||
let mut provider_entries: Vec<OpenAIProviderEntry> = Vec::new();
|
let mut provider_entries: Vec<OpenAIProviderEntry> = Vec::new();
|
||||||
@@ -1037,6 +1044,8 @@ impl LLMPreferences {
|
|||||||
true,
|
true,
|
||||||
base_url,
|
base_url,
|
||||||
api_key,
|
api_key,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
single_provider_models,
|
single_provider_models,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -1047,11 +1056,19 @@ impl LLMPreferences {
|
|||||||
.value()
|
.value()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|provider| {
|
.filter_map(|provider| {
|
||||||
if !provider.enabled
|
let missing_credentials = match provider.kind {
|
||||||
|| (provider.kind == OpenAIProviderKind::OpenAICompatible
|
OpenAIProviderKind::OpenAICompatible => provider.base_url.trim().is_empty(),
|
||||||
&& provider.base_url.trim().is_empty())
|
OpenAIProviderKind::Anthropic | OpenAIProviderKind::Gemini => provider
|
||||||
|| provider.models.is_empty()
|
.api_key
|
||||||
{
|
.as_deref()
|
||||||
|
.is_none_or(|key| key.trim().is_empty()),
|
||||||
|
OpenAIProviderKind::VertexAI => provider
|
||||||
|
.project_id
|
||||||
|
.as_deref()
|
||||||
|
.is_none_or(|project| project.trim().is_empty()),
|
||||||
|
OpenAIProviderKind::ChatGPTSubscription => false,
|
||||||
|
};
|
||||||
|
if !provider.enabled || missing_credentials || provider.models.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some((
|
Some((
|
||||||
@@ -1060,6 +1077,8 @@ impl LLMPreferences {
|
|||||||
provider.enabled,
|
provider.enabled,
|
||||||
provider.base_url.clone(),
|
provider.base_url.clone(),
|
||||||
provider.api_key.clone(),
|
provider.api_key.clone(),
|
||||||
|
provider.project_id.clone(),
|
||||||
|
provider.location.clone(),
|
||||||
provider.models.clone(),
|
provider.models.clone(),
|
||||||
))
|
))
|
||||||
}),
|
}),
|
||||||
@@ -1071,8 +1090,16 @@ impl LLMPreferences {
|
|||||||
|
|
||||||
let mut total_injected = 0;
|
let mut total_injected = 0;
|
||||||
let mut seen_model_ids: HashSet<String> = HashSet::new();
|
let mut seen_model_ids: HashSet<String> = HashSet::new();
|
||||||
for (provider_name, provider_kind, provider_enabled, base_url, api_key, models) in
|
for (
|
||||||
provider_entries
|
provider_name,
|
||||||
|
provider_kind,
|
||||||
|
provider_enabled,
|
||||||
|
base_url,
|
||||||
|
api_key,
|
||||||
|
provider_project_id,
|
||||||
|
provider_location,
|
||||||
|
models,
|
||||||
|
) in provider_entries
|
||||||
{
|
{
|
||||||
if !provider_enabled {
|
if !provider_enabled {
|
||||||
continue;
|
continue;
|
||||||
@@ -1109,12 +1136,14 @@ impl LLMPreferences {
|
|||||||
kind: provider_kind,
|
kind: provider_kind,
|
||||||
base_url: base_url.clone(),
|
base_url: base_url.clone(),
|
||||||
api_key: api_key.clone(),
|
api_key: api_key.clone(),
|
||||||
|
project_id: provider_project_id.clone(),
|
||||||
|
location: provider_location.clone(),
|
||||||
model: Some(model.model_id.clone()),
|
model: Some(model.model_id.clone()),
|
||||||
reasoning_effort: reasoning_effort.clone(),
|
reasoning_effort: reasoning_effort.clone(),
|
||||||
max_input_tokens: Some(openai_model_context_size(model)),
|
max_input_tokens: Some(openai_model_context_size(model)),
|
||||||
max_output_tokens: model.max_output_tokens,
|
max_output_tokens: model.max_output_tokens,
|
||||||
use_rig: model.use_rig
|
use_rig: model.use_rig
|
||||||
|| provider_kind == OpenAIProviderKind::ChatGPTSubscription,
|
|| !matches!(provider_kind, OpenAIProviderKind::OpenAICompatible),
|
||||||
supports_system_messages: model.supports_system_messages(),
|
supports_system_messages: model.supports_system_messages(),
|
||||||
};
|
};
|
||||||
self.openai_provider_routing
|
self.openai_provider_routing
|
||||||
@@ -1557,6 +1586,54 @@ impl LLMPreferences {
|
|||||||
pub(crate) async fn discover_openai_provider_models(
|
pub(crate) async fn discover_openai_provider_models(
|
||||||
provider: OpenAIProviderConfig,
|
provider: OpenAIProviderConfig,
|
||||||
) -> Result<Vec<OpenAIModelConfig>, String> {
|
) -> Result<Vec<OpenAIModelConfig>, String> {
|
||||||
|
let native_models = match provider.kind {
|
||||||
|
OpenAIProviderKind::Anthropic => {
|
||||||
|
let api_key = provider
|
||||||
|
.api_key
|
||||||
|
.as_deref()
|
||||||
|
.filter(|key| !key.trim().is_empty())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
"Enter an Anthropic API key before testing the connection.".to_string()
|
||||||
|
})?;
|
||||||
|
Some(discover_anthropic_models(api_key).await?)
|
||||||
|
}
|
||||||
|
OpenAIProviderKind::Gemini => {
|
||||||
|
let api_key = provider
|
||||||
|
.api_key
|
||||||
|
.as_deref()
|
||||||
|
.filter(|key| !key.trim().is_empty())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
"Enter a Gemini API key before testing the connection.".to_string()
|
||||||
|
})?;
|
||||||
|
Some(discover_gemini_models(api_key).await?)
|
||||||
|
}
|
||||||
|
OpenAIProviderKind::VertexAI => {
|
||||||
|
if provider
|
||||||
|
.project_id
|
||||||
|
.as_deref()
|
||||||
|
.is_none_or(|project| project.trim().is_empty())
|
||||||
|
{
|
||||||
|
return Err(
|
||||||
|
"Enter a Google Cloud project ID before testing the connection."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
validate_vertex_ai_credentials(
|
||||||
|
provider.project_id.as_deref().unwrap_or_default(),
|
||||||
|
provider.location.as_deref().unwrap_or("global"),
|
||||||
|
)?;
|
||||||
|
Some(vertex_ai_model_catalog())
|
||||||
|
}
|
||||||
|
OpenAIProviderKind::OpenAICompatible | OpenAIProviderKind::ChatGPTSubscription => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(models) = native_models {
|
||||||
|
if models.is_empty() {
|
||||||
|
return Err("The provider responded, but no models were found.".to_string());
|
||||||
|
}
|
||||||
|
return Ok(Self::rig_models_to_openai_models(models));
|
||||||
|
}
|
||||||
|
|
||||||
if provider.base_url.trim().is_empty() {
|
if provider.base_url.trim().is_empty() {
|
||||||
return Err("Enter a provider URL before testing the connection.".to_string());
|
return Err("Enter a provider URL before testing the connection.".to_string());
|
||||||
}
|
}
|
||||||
@@ -1586,6 +1663,26 @@ impl LLMPreferences {
|
|||||||
Ok(models)
|
Ok(models)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
fn rig_models_to_openai_models(models: Vec<RigModelInfo>) -> Vec<OpenAIModelConfig> {
|
||||||
|
models
|
||||||
|
.into_iter()
|
||||||
|
.map(|model| OpenAIModelConfig {
|
||||||
|
model_id: model.id,
|
||||||
|
display_name: model.display_name,
|
||||||
|
vision_supported: false,
|
||||||
|
context_size: model.context_size.unwrap_or(128_000),
|
||||||
|
max_input_tokens: model.context_size,
|
||||||
|
max_output_tokens: None,
|
||||||
|
provider: None,
|
||||||
|
use_rig: true,
|
||||||
|
supports_system_messages: Some(true),
|
||||||
|
reasoning_efforts: Vec::new(),
|
||||||
|
enabled: true,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the `LLMInfo` for the base LLM to be used for an Agent Mode request.
|
/// Returns the `LLMInfo` for the base LLM to be used for an Agent Mode request.
|
||||||
pub fn get_active_base_model<'a>(
|
pub fn get_active_base_model<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ pub struct OpenAIClientConfig {
|
|||||||
pub kind: OpenAIProviderKind,
|
pub kind: OpenAIProviderKind,
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
|
pub project_id: Option<String>,
|
||||||
|
pub location: Option<String>,
|
||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
pub reasoning_effort: Option<String>,
|
pub reasoning_effort: Option<String>,
|
||||||
pub max_input_tokens: Option<u32>,
|
pub max_input_tokens: Option<u32>,
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ use galaxy_agent_core::{
|
|||||||
ToolCallDecision, ToolEvent, ToolPolicy, ToolResult, TurnCommand,
|
ToolCallDecision, ToolEvent, ToolPolicy, ToolResult, TurnCommand,
|
||||||
};
|
};
|
||||||
use galaxy_agent_rig::{
|
use galaxy_agent_rig::{
|
||||||
ChatGPTSubscriptionRuntime, ChatGPTSubscriptionRuntimeConfig, OpenAICompatibleRuntime,
|
AnthropicRuntime, AnthropicRuntimeConfig, ChatGPTSubscriptionRuntime,
|
||||||
OpenAICompatibleRuntimeConfig,
|
ChatGPTSubscriptionRuntimeConfig, GeminiRuntime, GeminiRuntimeConfig, OpenAICompatibleRuntime,
|
||||||
|
OpenAICompatibleRuntimeConfig, VertexAiRuntime, VertexAiRuntimeConfig,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use warp_multi_agent_api::ToolType;
|
use warp_multi_agent_api::ToolType;
|
||||||
@@ -73,6 +74,52 @@ pub(crate) fn rig_openai_response_stream(
|
|||||||
cancellation_rx,
|
cancellation_rx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
OpenAIProviderKind::Anthropic => {
|
||||||
|
let runtime = AnthropicRuntime::new(AnthropicRuntimeConfig {
|
||||||
|
api_key: config.api_key.unwrap_or_default(),
|
||||||
|
model: model_id,
|
||||||
|
max_output_tokens: config.max_output_tokens.map(u64::from),
|
||||||
|
});
|
||||||
|
rig_response_stream(
|
||||||
|
runtime,
|
||||||
|
prepared,
|
||||||
|
skill_path_origin,
|
||||||
|
config.max_input_tokens,
|
||||||
|
"rig_anthropic",
|
||||||
|
cancellation_rx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OpenAIProviderKind::Gemini => {
|
||||||
|
let runtime = GeminiRuntime::new(GeminiRuntimeConfig {
|
||||||
|
api_key: config.api_key.unwrap_or_default(),
|
||||||
|
model: model_id,
|
||||||
|
max_output_tokens: config.max_output_tokens.map(u64::from),
|
||||||
|
});
|
||||||
|
rig_response_stream(
|
||||||
|
runtime,
|
||||||
|
prepared,
|
||||||
|
skill_path_origin,
|
||||||
|
config.max_input_tokens,
|
||||||
|
"rig_gemini",
|
||||||
|
cancellation_rx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OpenAIProviderKind::VertexAI => {
|
||||||
|
let runtime = VertexAiRuntime::new(VertexAiRuntimeConfig {
|
||||||
|
project_id: config.project_id.unwrap_or_default(),
|
||||||
|
location: config.location.unwrap_or_else(|| "global".to_string()),
|
||||||
|
model: model_id,
|
||||||
|
max_output_tokens: config.max_output_tokens.map(u64::from),
|
||||||
|
});
|
||||||
|
rig_response_stream(
|
||||||
|
runtime,
|
||||||
|
prepared,
|
||||||
|
skill_path_origin,
|
||||||
|
config.max_input_tokens,
|
||||||
|
"rig_vertex_ai",
|
||||||
|
cancellation_rx,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ fn config() -> OpenAIClientConfig {
|
|||||||
kind: crate::settings::OpenAIProviderKind::OpenAICompatible,
|
kind: crate::settings::OpenAIProviderKind::OpenAICompatible,
|
||||||
base_url: "http://localhost:4000/v1".to_string(),
|
base_url: "http://localhost:4000/v1".to_string(),
|
||||||
api_key: None,
|
api_key: None,
|
||||||
|
project_id: None,
|
||||||
|
location: None,
|
||||||
model: Some("provider-model".to_string()),
|
model: Some("provider-model".to_string()),
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
max_input_tokens: Some(128_000),
|
max_input_tokens: Some(128_000),
|
||||||
|
|||||||
@@ -1,32 +1,12 @@
|
|||||||
use std::env::current_dir;
|
use std::env::current_dir;
|
||||||
|
|
||||||
use galaxy_core::ui::appearance::Appearance;
|
|
||||||
use galaxyui::App;
|
use galaxyui::App;
|
||||||
|
|
||||||
use super::expand_dirs;
|
use super::expand_dirs;
|
||||||
use crate::cloud_object::model::persistence::CloudModel;
|
|
||||||
use crate::network::NetworkStatus;
|
|
||||||
use crate::server::cloud_objects::update_manager::UpdateManager;
|
|
||||||
use crate::server::sync_queue::SyncQueue;
|
|
||||||
use crate::workspaces::team_tester::TeamTesterStatus;
|
|
||||||
use crate::workspaces::user_workspaces::UserWorkspaces;
|
|
||||||
use crate::{GlobalResourceHandles, GlobalResourceHandlesProvider};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_expand_directories() {
|
fn test_expand_directories() {
|
||||||
App::test((), |mut app| async move {
|
App::test((), |_| async move {
|
||||||
app.update(crate::settings::init_and_register_user_preferences);
|
|
||||||
|
|
||||||
let global_resource_handles = GlobalResourceHandles::mock(&mut app);
|
|
||||||
app.add_singleton_model(|_| GlobalResourceHandlesProvider::new(global_resource_handles));
|
|
||||||
app.add_singleton_model(CloudModel::mock);
|
|
||||||
app.add_singleton_model(UserWorkspaces::default_mock);
|
|
||||||
app.add_singleton_model(|_| Appearance::mock());
|
|
||||||
app.add_singleton_model(|_| NetworkStatus::new());
|
|
||||||
app.add_singleton_model(SyncQueue::mock);
|
|
||||||
app.add_singleton_model(TeamTesterStatus::mock);
|
|
||||||
app.add_singleton_model(UpdateManager::mock);
|
|
||||||
|
|
||||||
let directory = current_dir()
|
let directory = current_dir()
|
||||||
.expect("current directory should exist")
|
.expect("current directory should exist")
|
||||||
.parent()
|
.parent()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use super::modal_body::{ImportModalBody, ImportModalBodyAction, ImportModalBodyE
|
|||||||
use crate::appearance::Appearance;
|
use crate::appearance::Appearance;
|
||||||
use crate::cloud_object::model::persistence::CloudModel;
|
use crate::cloud_object::model::persistence::CloudModel;
|
||||||
use crate::cloud_object::{CloudObject, Owner};
|
use crate::cloud_object::{CloudObject, Owner};
|
||||||
|
use crate::local_object_repository::local_owner;
|
||||||
use crate::server::ids::SyncId;
|
use crate::server::ids::SyncId;
|
||||||
use crate::server::sync_queue::SyncQueue;
|
use crate::server::sync_queue::SyncQueue;
|
||||||
use crate::themes::theme::GalaxyTheme;
|
use crate::themes::theme::GalaxyTheme;
|
||||||
@@ -88,7 +89,8 @@ impl ImportModal {
|
|||||||
let window_id = ctx.window_id();
|
let window_id = ctx.window_id();
|
||||||
let import_body_id = self.import_modal.id();
|
let import_body_id = self.import_modal.id();
|
||||||
|
|
||||||
let sync_queue_is_dequeueing = SyncQueue::as_ref(ctx).is_dequeueing();
|
let sync_queue_is_dequeueing =
|
||||||
|
self.owner != Some(local_owner()) && SyncQueue::as_ref(ctx).is_dequeueing();
|
||||||
|
|
||||||
let allowed_file_types = vec![FileType::Yaml, FileType::Markdown];
|
let allowed_file_types = vec![FileType::Yaml, FileType::Markdown];
|
||||||
|
|
||||||
@@ -98,7 +100,7 @@ impl ImportModal {
|
|||||||
|
|
||||||
// Files under a folder could only be uploaded when the folder is created on the server.
|
// Files under a folder could only be uploaded when the folder is created on the server.
|
||||||
// When sync queue is not dequeueing, disable folder upload in the import modal.
|
// When sync queue is not dequeueing, disable folder upload in the import modal.
|
||||||
if sync_queue_is_dequeueing {
|
if sync_queue_is_dequeueing || self.owner == Some(local_owner()) {
|
||||||
file_picker_config = file_picker_config.allow_folder();
|
file_picker_config = file_picker_config.allow_folder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,9 +167,13 @@ impl ImportModal {
|
|||||||
// Convert to a Space for display, in case we're importing into a shared folder.
|
// Convert to a Space for display, in case we're importing into a shared folder.
|
||||||
self.owner
|
self.owner
|
||||||
.map(|owner| {
|
.map(|owner| {
|
||||||
UserWorkspaces::as_ref(app)
|
if owner == local_owner() {
|
||||||
.owner_to_space(owner, app)
|
"Personal".to_string()
|
||||||
.name(app)
|
} else {
|
||||||
|
UserWorkspaces::as_ref(app)
|
||||||
|
.owner_to_space(owner, app)
|
||||||
|
.name(app)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use super::nodes::{
|
|||||||
use super::queue::{ImportQueue, ImportQueueArgs, ImportQueueEvent, ParentId, RequestContent};
|
use super::queue::{ImportQueue, ImportQueueArgs, ImportQueueEvent, ParentId, RequestContent};
|
||||||
use crate::appearance::Appearance;
|
use crate::appearance::Appearance;
|
||||||
use crate::cloud_object::Owner;
|
use crate::cloud_object::Owner;
|
||||||
|
use crate::local_object_repository::local_owner;
|
||||||
use crate::server::ids::{ClientId, SyncId};
|
use crate::server::ids::{ClientId, SyncId};
|
||||||
use crate::server::sync_queue::SyncQueue;
|
use crate::server::sync_queue::SyncQueue;
|
||||||
use crate::ui_components::icons::Icon;
|
use crate::ui_components::icons::Icon;
|
||||||
@@ -96,7 +97,7 @@ pub struct ImportModalBody {
|
|||||||
|
|
||||||
impl ImportModalBody {
|
impl ImportModalBody {
|
||||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||||
let import_queue = ctx.add_model(ImportQueue::new);
|
let import_queue = ctx.add_model(|_| ImportQueue::new());
|
||||||
ctx.subscribe_to_model(&import_queue, |me, _, event, ctx| {
|
ctx.subscribe_to_model(&import_queue, |me, _, event, ctx| {
|
||||||
me.handle_import_queue_event(event, ctx)
|
me.handle_import_queue_event(event, ctx)
|
||||||
});
|
});
|
||||||
@@ -152,7 +153,8 @@ impl ImportModalBody {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let sync_queue_dequeueing = SyncQueue::as_ref(ctx).is_dequeueing();
|
let sync_queue_dequeueing =
|
||||||
|
self.owner != Some(local_owner()) && SyncQueue::as_ref(ctx).is_dequeueing();
|
||||||
|
|
||||||
if !sync_queue_dequeueing && state.all_files_saved_locally() {
|
if !sync_queue_dequeueing && state.all_files_saved_locally() {
|
||||||
ctx.emit(ImportModalBodyEvent::AllFileSavedLocally);
|
ctx.emit(ImportModalBodyEvent::AllFileSavedLocally);
|
||||||
@@ -177,7 +179,8 @@ impl ImportModalBody {
|
|||||||
// Whether there is an active upload in progress (If all uploads are completed,
|
// Whether there is an active upload in progress (If all uploads are completed,
|
||||||
// we don't consider the import modal upload to be in progress).
|
// we don't consider the import modal upload to be in progress).
|
||||||
pub fn upload_in_progress(&self, app: &AppContext) -> bool {
|
pub fn upload_in_progress(&self, app: &AppContext) -> bool {
|
||||||
let sync_queue_dequeueing = SyncQueue::as_ref(app).is_dequeueing();
|
let sync_queue_dequeueing =
|
||||||
|
self.owner != Some(local_owner()) && SyncQueue::as_ref(app).is_dequeueing();
|
||||||
|
|
||||||
match &self.state {
|
match &self.state {
|
||||||
ImportState::Upload => false,
|
ImportState::Upload => false,
|
||||||
@@ -498,7 +501,8 @@ impl View for ImportModalBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||||
let sync_queue_dequeueing = SyncQueue::as_ref(app).is_dequeueing();
|
let sync_queue_dequeueing =
|
||||||
|
self.owner != Some(local_owner()) && SyncQueue::as_ref(app).is_dequeueing();
|
||||||
let appearance = Appearance::as_ref(app);
|
let appearance = Appearance::as_ref(app);
|
||||||
|
|
||||||
match &self.state {
|
match &self.state {
|
||||||
|
|||||||
+165
-77
@@ -5,7 +5,7 @@ use galaxyui::{Entity, ModelContext, SingletonEntity};
|
|||||||
use super::nodes::{self, FileId};
|
use super::nodes::{self, FileId};
|
||||||
use crate::cloud_object::model::persistence::CloudModel;
|
use crate::cloud_object::model::persistence::CloudModel;
|
||||||
use crate::cloud_object::{CloudObjectEventEntrypoint, Owner};
|
use crate::cloud_object::{CloudObjectEventEntrypoint, Owner};
|
||||||
use crate::drive::folders::FolderId;
|
use crate::local_object_repository::{local_owner, LocalObjectRepository};
|
||||||
use crate::notebooks::CloudNotebookModel;
|
use crate::notebooks::CloudNotebookModel;
|
||||||
use crate::server::cloud_objects::update_manager::{
|
use crate::server::cloud_objects::update_manager::{
|
||||||
InitiatedBy, ObjectOperation, OperationSuccessType, UpdateManager, UpdateManagerEvent,
|
InitiatedBy, ObjectOperation, OperationSuccessType, UpdateManager, UpdateManagerEvent,
|
||||||
@@ -94,31 +94,40 @@ impl FileCompletionCounter {
|
|||||||
|
|
||||||
pub(super) struct ImportQueue {
|
pub(super) struct ImportQueue {
|
||||||
queue: Vec<ImportQueueArgs>,
|
queue: Vec<ImportQueueArgs>,
|
||||||
client_to_server_id: HashMap<ClientId, Option<FolderId>>,
|
client_to_folder_id: HashMap<ClientId, Option<SyncId>>,
|
||||||
client_to_node_folder_id: HashMap<ClientId, nodes::FolderId>,
|
client_to_node_folder_id: HashMap<ClientId, nodes::FolderId>,
|
||||||
file_completion: FileCompletionCounter,
|
file_completion: FileCompletionCounter,
|
||||||
|
remote_subscription_initialized: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImportQueue {
|
impl ImportQueue {
|
||||||
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
queue: Vec::new(),
|
||||||
|
client_to_folder_id: HashMap::default(),
|
||||||
|
file_completion: Default::default(),
|
||||||
|
client_to_node_folder_id: HashMap::default(),
|
||||||
|
remote_subscription_initialized: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_remote_subscription(&mut self, ctx: &mut ModelContext<Self>) {
|
||||||
|
if self.remote_subscription_initialized {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let update_manager = UpdateManager::handle(ctx);
|
let update_manager = UpdateManager::handle(ctx);
|
||||||
ctx.subscribe_to_model(&update_manager, |me, _, event, ctx| {
|
ctx.subscribe_to_model(&update_manager, |me, _, event, ctx| {
|
||||||
me.handle_update_manager_event(event, ctx);
|
me.handle_update_manager_event(event, ctx);
|
||||||
});
|
});
|
||||||
|
self.remote_subscription_initialized = true;
|
||||||
Self {
|
|
||||||
queue: Vec::new(),
|
|
||||||
client_to_server_id: HashMap::default(),
|
|
||||||
file_completion: Default::default(),
|
|
||||||
client_to_node_folder_id: HashMap::default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether all dependencies of an item has been sync-ed.
|
// Whether all dependencies of an item has been sync-ed.
|
||||||
fn dependency_synced(&self, item: &ImportQueueArgs) -> bool {
|
fn dependency_synced(&self, item: &ImportQueueArgs) -> bool {
|
||||||
match &item.parent_id {
|
match &item.parent_id {
|
||||||
ParentId::FolderToUpload(id) => self
|
ParentId::FolderToUpload(id) => self
|
||||||
.client_to_server_id
|
.client_to_folder_id
|
||||||
.get(id)
|
.get(id)
|
||||||
.map(|item| item.is_some())
|
.map(|item| item.is_some())
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
@@ -128,6 +137,11 @@ impl ImportQueue {
|
|||||||
|
|
||||||
// Enqueue a new request to the import queue.
|
// Enqueue a new request to the import queue.
|
||||||
pub fn enqueue(&mut self, arg: ImportQueueArgs, ctx: &mut ModelContext<Self>) {
|
pub fn enqueue(&mut self, arg: ImportQueueArgs, ctx: &mut ModelContext<Self>) {
|
||||||
|
let is_local = arg.owner == local_owner();
|
||||||
|
if !is_local {
|
||||||
|
self.ensure_remote_subscription(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
// Update internal tracker of the object.
|
// Update internal tracker of the object.
|
||||||
match &arg.content {
|
match &arg.content {
|
||||||
RequestContent::Folder {
|
RequestContent::Folder {
|
||||||
@@ -135,17 +149,23 @@ impl ImportQueue {
|
|||||||
folder_id,
|
folder_id,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
self.client_to_server_id.insert(*client_id, None);
|
self.client_to_folder_id.insert(*client_id, None);
|
||||||
self.client_to_node_folder_id.insert(*client_id, *folder_id);
|
self.client_to_node_folder_id.insert(*client_id, *folder_id);
|
||||||
}
|
}
|
||||||
RequestContent::Notebook {
|
RequestContent::Notebook {
|
||||||
client_id, file_id, ..
|
client_id, file_id, ..
|
||||||
} => self.file_completion.add_entry(*client_id, *file_id),
|
} => {
|
||||||
|
if !is_local {
|
||||||
|
self.file_completion.add_entry(*client_id, *file_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
RequestContent::Workflow {
|
RequestContent::Workflow {
|
||||||
workflows, file_id, ..
|
workflows, file_id, ..
|
||||||
} => {
|
} => {
|
||||||
for (_, client_id) in workflows {
|
if !is_local {
|
||||||
self.file_completion.add_entry(*client_id, *file_id);
|
for (_, client_id) in workflows {
|
||||||
|
self.file_completion.add_entry(*client_id, *file_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,31 +187,47 @@ impl ImportQueue {
|
|||||||
{
|
{
|
||||||
let dequeued_item = self.queue.remove(idx);
|
let dequeued_item = self.queue.remove(idx);
|
||||||
let parent_id = match dequeued_item.parent_id {
|
let parent_id = match dequeued_item.parent_id {
|
||||||
ParentId::FolderToUpload(client_id) => Some(SyncId::ServerId(
|
ParentId::FolderToUpload(client_id) => Some(
|
||||||
self.client_to_server_id
|
self.client_to_folder_id
|
||||||
.get(&client_id)
|
.get(&client_id)
|
||||||
.expect("Client id entry should exist")
|
.expect("Client id entry should exist")
|
||||||
.expect("Server id entry should exist")
|
.expect("Folder id entry should exist"),
|
||||||
.into(),
|
),
|
||||||
)),
|
|
||||||
ParentId::InitialFolder(folder_id) => folder_id,
|
ParentId::InitialFolder(folder_id) => folder_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let is_local = dequeued_item.owner == local_owner();
|
||||||
match dequeued_item.content {
|
match dequeued_item.content {
|
||||||
RequestContent::Folder {
|
RequestContent::Folder {
|
||||||
name, client_id, ..
|
name, client_id, ..
|
||||||
} => {
|
} => {
|
||||||
UpdateManager::handle(ctx).update(ctx, |update_manager, ctx| {
|
if is_local {
|
||||||
update_manager.create_folder(
|
let local_id = SyncId::ClientId(client_id);
|
||||||
name,
|
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
|
||||||
dequeued_item.owner,
|
repository.create_folder_with_id(local_id, name, parent_id, ctx);
|
||||||
client_id,
|
});
|
||||||
parent_id,
|
self.client_to_folder_id.insert(client_id, Some(local_id));
|
||||||
false,
|
ctx.emit(ImportQueueEvent::FolderCompleted {
|
||||||
InitiatedBy::User,
|
folder_id: self
|
||||||
ctx,
|
.client_to_node_folder_id
|
||||||
);
|
.get(&client_id)
|
||||||
});
|
.copied()
|
||||||
|
.expect("Folder node id should exist"),
|
||||||
|
server_id: Some(local_id.uid()),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
UpdateManager::handle(ctx).update(ctx, |update_manager, ctx| {
|
||||||
|
update_manager.create_folder(
|
||||||
|
name,
|
||||||
|
dequeued_item.owner,
|
||||||
|
client_id,
|
||||||
|
parent_id,
|
||||||
|
false,
|
||||||
|
InitiatedBy::User,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
RequestContent::Notebook {
|
RequestContent::Notebook {
|
||||||
title,
|
title,
|
||||||
@@ -199,56 +235,104 @@ impl ImportQueue {
|
|||||||
client_id,
|
client_id,
|
||||||
file_id,
|
file_id,
|
||||||
} => {
|
} => {
|
||||||
UpdateManager::handle(ctx).update(ctx, |update_manager, ctx| {
|
if is_local {
|
||||||
update_manager.create_notebook(
|
let local_id = SyncId::ClientId(client_id);
|
||||||
client_id,
|
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
|
||||||
dequeued_item.owner,
|
repository.create_notebook_with_id(
|
||||||
parent_id,
|
local_id,
|
||||||
CloudNotebookModel {
|
parent_id,
|
||||||
title,
|
CloudNotebookModel {
|
||||||
data,
|
title,
|
||||||
ai_document_id: None,
|
data,
|
||||||
conversation_id: None,
|
ai_document_id: None,
|
||||||
},
|
conversation_id: None,
|
||||||
CloudObjectEventEntrypoint::ImportModal,
|
},
|
||||||
false,
|
ctx,
|
||||||
ctx,
|
);
|
||||||
);
|
});
|
||||||
});
|
ctx.emit(ImportQueueEvent::FileCompleted {
|
||||||
ctx.emit(ImportQueueEvent::FileSavedLocally(file_id));
|
file_id,
|
||||||
|
server_id: Some(local_id.uid()),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
UpdateManager::handle(ctx).update(ctx, |update_manager, ctx| {
|
||||||
|
update_manager.create_notebook(
|
||||||
|
client_id,
|
||||||
|
dequeued_item.owner,
|
||||||
|
parent_id,
|
||||||
|
CloudNotebookModel {
|
||||||
|
title,
|
||||||
|
data,
|
||||||
|
ai_document_id: None,
|
||||||
|
conversation_id: None,
|
||||||
|
},
|
||||||
|
CloudObjectEventEntrypoint::ImportModal,
|
||||||
|
false,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ctx.emit(ImportQueueEvent::FileSavedLocally(file_id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
RequestContent::Workflow {
|
RequestContent::Workflow {
|
||||||
workflows,
|
workflows,
|
||||||
workflow_enums,
|
workflow_enums,
|
||||||
file_id,
|
file_id,
|
||||||
} => {
|
} => {
|
||||||
UpdateManager::handle(ctx).update(ctx, |update_manager, ctx| {
|
if is_local {
|
||||||
// Create any new workflow enums
|
let local_object_id = workflows
|
||||||
for (client_id, workflow_enum) in workflow_enums {
|
.first()
|
||||||
update_manager.create_workflow_enum(
|
.map(|(_, client_id)| SyncId::ClientId(*client_id).uid());
|
||||||
workflow_enum,
|
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
|
||||||
dequeued_item.owner,
|
for (client_id, workflow_enum) in workflow_enums {
|
||||||
client_id,
|
repository.create_workflow_enum_with_id(
|
||||||
CloudObjectEventEntrypoint::ImportModal,
|
SyncId::ClientId(client_id),
|
||||||
false,
|
workflow_enum,
|
||||||
ctx,
|
ctx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
for (workflow, client_id) in workflows {
|
||||||
|
repository.create_workflow_with_id(
|
||||||
|
SyncId::ClientId(client_id),
|
||||||
|
parent_id,
|
||||||
|
workflow,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctx.emit(ImportQueueEvent::FileCompleted {
|
||||||
|
file_id,
|
||||||
|
server_id: local_object_id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
UpdateManager::handle(ctx).update(ctx, |update_manager, ctx| {
|
||||||
|
// Create any new workflow enums
|
||||||
|
for (client_id, workflow_enum) in workflow_enums {
|
||||||
|
update_manager.create_workflow_enum(
|
||||||
|
workflow_enum,
|
||||||
|
dequeued_item.owner,
|
||||||
|
client_id,
|
||||||
|
CloudObjectEventEntrypoint::ImportModal,
|
||||||
|
false,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Create the workflow
|
// Create the workflow
|
||||||
for (workflow, client_id) in workflows {
|
for (workflow, client_id) in workflows {
|
||||||
update_manager.create_workflow(
|
update_manager.create_workflow(
|
||||||
workflow,
|
workflow,
|
||||||
dequeued_item.owner,
|
dequeued_item.owner,
|
||||||
parent_id,
|
parent_id,
|
||||||
client_id,
|
client_id,
|
||||||
CloudObjectEventEntrypoint::ImportModal,
|
CloudObjectEventEntrypoint::ImportModal,
|
||||||
false,
|
false,
|
||||||
ctx,
|
ctx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ctx.emit(ImportQueueEvent::FileSavedLocally(file_id));
|
ctx.emit(ImportQueueEvent::FileSavedLocally(file_id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.dequeue(ctx);
|
self.dequeue(ctx);
|
||||||
@@ -294,14 +378,14 @@ impl ImportQueue {
|
|||||||
|
|
||||||
let Some(folder_id) = cloud_model
|
let Some(folder_id) = cloud_model
|
||||||
.get_folder_by_uid(&result.server_id.expect("Expect id").uid())
|
.get_folder_by_uid(&result.server_id.expect("Expect id").uid())
|
||||||
.and_then(|folder| folder.id.into_server())
|
.map(|folder| folder.id)
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let replaced = match self.client_to_server_id.get_mut(&client_id) {
|
let replaced = match self.client_to_folder_id.get_mut(&client_id) {
|
||||||
Some(value) if value.is_none() => {
|
Some(value) if value.is_none() => {
|
||||||
*value = Some(folder_id.into());
|
*value = Some(folder_id);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
@@ -323,3 +407,7 @@ impl ImportQueue {
|
|||||||
impl Entity for ImportQueue {
|
impl Entity for ImportQueue {
|
||||||
type Event = ImportQueueEvent;
|
type Event = ImportQueueEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "queue_tests.rs"]
|
||||||
|
mod tests;
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use galaxyui::{App, SingletonEntity};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::cloud_object::model::persistence::CloudModel;
|
||||||
|
use crate::local_object_repository::local_owner;
|
||||||
|
use crate::server::ids::ClientId;
|
||||||
|
use crate::workflows::workflow_enum::{EnumVariants, WorkflowEnum};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
enum EventKind {
|
||||||
|
Folder {
|
||||||
|
folder_id: nodes::FolderId,
|
||||||
|
object_id: Option<String>,
|
||||||
|
},
|
||||||
|
File {
|
||||||
|
file_id: FileId,
|
||||||
|
object_id: Option<String>,
|
||||||
|
},
|
||||||
|
FileSavedLocally(FileId),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_import_queue_persists_nested_content_and_reports_completion() {
|
||||||
|
App::test((), |mut app| async move {
|
||||||
|
app.add_singleton_model(|_| CloudModel::new(None, Vec::new(), None));
|
||||||
|
app.add_singleton_model(|ctx| {
|
||||||
|
crate::local_object_repository::LocalObjectRepository::new(None, None, ctx)
|
||||||
|
});
|
||||||
|
|
||||||
|
let queue = app.add_model(|_| ImportQueue::new());
|
||||||
|
let events = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let events_for_subscription = events.clone();
|
||||||
|
app.update(|ctx| {
|
||||||
|
ctx.subscribe_to_model(&queue, move |_, event: &ImportQueueEvent, _| {
|
||||||
|
let event = match event {
|
||||||
|
ImportQueueEvent::FolderCompleted {
|
||||||
|
folder_id,
|
||||||
|
server_id,
|
||||||
|
} => EventKind::Folder {
|
||||||
|
folder_id: *folder_id,
|
||||||
|
object_id: server_id.clone(),
|
||||||
|
},
|
||||||
|
ImportQueueEvent::FileCompleted { file_id, server_id } => EventKind::File {
|
||||||
|
file_id: *file_id,
|
||||||
|
object_id: server_id.clone(),
|
||||||
|
},
|
||||||
|
ImportQueueEvent::FileSavedLocally(file_id) => {
|
||||||
|
EventKind::FileSavedLocally(*file_id)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
events_for_subscription.borrow_mut().push(event);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let parent_client_id = ClientId::new();
|
||||||
|
let child_client_id = ClientId::new();
|
||||||
|
let notebook_client_id = ClientId::new();
|
||||||
|
let workflow_client_id = ClientId::new();
|
||||||
|
let workflow_enum_client_id = ClientId::new();
|
||||||
|
|
||||||
|
queue.update(&mut app, |queue, ctx| {
|
||||||
|
queue.enqueue(
|
||||||
|
ImportQueueArgs {
|
||||||
|
owner: local_owner(),
|
||||||
|
parent_id: ParentId::InitialFolder(None),
|
||||||
|
content: RequestContent::Folder {
|
||||||
|
name: "Imported".to_string(),
|
||||||
|
client_id: parent_client_id,
|
||||||
|
folder_id: nodes::FolderId::from(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
queue.enqueue(
|
||||||
|
ImportQueueArgs {
|
||||||
|
owner: local_owner(),
|
||||||
|
parent_id: ParentId::FolderToUpload(parent_client_id),
|
||||||
|
content: RequestContent::Folder {
|
||||||
|
name: "Nested".to_string(),
|
||||||
|
client_id: child_client_id,
|
||||||
|
folder_id: nodes::FolderId::from(2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
queue.enqueue(
|
||||||
|
ImportQueueArgs {
|
||||||
|
owner: local_owner(),
|
||||||
|
parent_id: ParentId::FolderToUpload(child_client_id),
|
||||||
|
content: RequestContent::Notebook {
|
||||||
|
title: "Imported notes".to_string(),
|
||||||
|
data: "hello".to_string(),
|
||||||
|
client_id: notebook_client_id,
|
||||||
|
file_id: FileId(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
queue.enqueue(
|
||||||
|
ImportQueueArgs {
|
||||||
|
owner: local_owner(),
|
||||||
|
parent_id: ParentId::FolderToUpload(child_client_id),
|
||||||
|
content: RequestContent::Workflow {
|
||||||
|
workflows: vec![(
|
||||||
|
crate::workflows::workflow::Workflow::new(
|
||||||
|
"Imported workflow",
|
||||||
|
"echo imported",
|
||||||
|
),
|
||||||
|
workflow_client_id,
|
||||||
|
)],
|
||||||
|
workflow_enums: HashMap::from([(
|
||||||
|
workflow_enum_client_id,
|
||||||
|
WorkflowEnum {
|
||||||
|
name: "Environment".to_string(),
|
||||||
|
is_shared: false,
|
||||||
|
variants: EnumVariants::Static(vec!["dev".to_string()]),
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
file_id: FileId(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let parent_id = SyncId::ClientId(parent_client_id);
|
||||||
|
let child_id = SyncId::ClientId(child_client_id);
|
||||||
|
let notebook_id = SyncId::ClientId(notebook_client_id);
|
||||||
|
let workflow_id = SyncId::ClientId(workflow_client_id);
|
||||||
|
let workflow_enum_id = SyncId::ClientId(workflow_enum_client_id);
|
||||||
|
|
||||||
|
app.update(|ctx| {
|
||||||
|
let cloud_model = CloudModel::as_ref(ctx);
|
||||||
|
let parent = cloud_model.get_folder(&parent_id).expect("parent folder");
|
||||||
|
assert_eq!(parent.permissions.owner, local_owner());
|
||||||
|
assert_eq!(
|
||||||
|
cloud_model
|
||||||
|
.get_folder(&child_id)
|
||||||
|
.unwrap()
|
||||||
|
.metadata
|
||||||
|
.folder_id,
|
||||||
|
Some(parent_id)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cloud_model
|
||||||
|
.get_notebook(¬ebook_id)
|
||||||
|
.unwrap()
|
||||||
|
.metadata
|
||||||
|
.folder_id,
|
||||||
|
Some(child_id)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cloud_model
|
||||||
|
.get_workflow(&workflow_id)
|
||||||
|
.unwrap()
|
||||||
|
.metadata
|
||||||
|
.folder_id,
|
||||||
|
Some(child_id)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cloud_model
|
||||||
|
.get_workflow_enum(&workflow_enum_id)
|
||||||
|
.unwrap()
|
||||||
|
.model()
|
||||||
|
.string_model
|
||||||
|
.name,
|
||||||
|
"Environment"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let events = events.borrow();
|
||||||
|
assert!(events.contains(&EventKind::Folder {
|
||||||
|
folder_id: nodes::FolderId::from(1),
|
||||||
|
object_id: Some(parent_id.uid()),
|
||||||
|
}));
|
||||||
|
assert!(events.contains(&EventKind::Folder {
|
||||||
|
folder_id: nodes::FolderId::from(2),
|
||||||
|
object_id: Some(child_id.uid()),
|
||||||
|
}));
|
||||||
|
assert!(events.contains(&EventKind::File {
|
||||||
|
file_id: FileId(0),
|
||||||
|
object_id: Some(notebook_id.uid()),
|
||||||
|
}));
|
||||||
|
assert!(events.contains(&EventKind::File {
|
||||||
|
file_id: FileId(1),
|
||||||
|
object_id: Some(workflow_id.uid()),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
+100
-21
@@ -68,7 +68,7 @@ use crate::drive::panel::DrivePanelAction;
|
|||||||
use crate::editor::{EditorView, Event as EditorEvent, SingleLineEditorOptions};
|
use crate::editor::{EditorView, Event as EditorEvent, SingleLineEditorOptions};
|
||||||
use crate::env_vars::CloudEnvVarCollection;
|
use crate::env_vars::CloudEnvVarCollection;
|
||||||
use crate::features::FeatureFlag;
|
use crate::features::FeatureFlag;
|
||||||
use crate::local_object_repository::LocalObjectRepository;
|
use crate::local_object_repository::{local_owner, LocalObjectRepository};
|
||||||
use crate::menu::{Event, Menu, MenuItem, MenuItemFields};
|
use crate::menu::{Event, Menu, MenuItem, MenuItemFields};
|
||||||
use crate::network::NetworkStatus;
|
use crate::network::NetworkStatus;
|
||||||
use crate::notebooks::CloudNotebookModel;
|
use crate::notebooks::CloudNotebookModel;
|
||||||
@@ -1064,6 +1064,12 @@ impl DriveIndex {
|
|||||||
NetworkStatus::as_ref(app).is_online()
|
NetworkStatus::as_ref(app).is_online()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_local_folder(folder_id: &SyncId, app: &AppContext) -> bool {
|
||||||
|
CloudModel::as_ref(app)
|
||||||
|
.get_folder(folder_id)
|
||||||
|
.is_some_and(|folder| folder.permissions.owner == local_owner())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scroll_item_into_view(&mut self, item_id: WarpDriveItemId, ctx: &mut ViewContext<Self>) {
|
pub fn scroll_item_into_view(&mut self, item_id: WarpDriveItemId, ctx: &mut ViewContext<Self>) {
|
||||||
self.clipped_scroll_state.scroll_to_position(ScrollTarget {
|
self.clipped_scroll_state.scroll_to_position(ScrollTarget {
|
||||||
position_id: item_id.drive_row_position_id(),
|
position_id: item_id.drive_row_position_id(),
|
||||||
@@ -3330,9 +3336,15 @@ impl DriveIndex {
|
|||||||
match new_location {
|
match new_location {
|
||||||
CloudObjectLocation::Space(space) => self.open_section_of_space(space),
|
CloudObjectLocation::Space(space) => self.open_section_of_space(space),
|
||||||
CloudObjectLocation::Folder(folder_id) => {
|
CloudObjectLocation::Folder(folder_id) => {
|
||||||
cloud_model.update(ctx, |cloud_model, ctx| {
|
if Self::is_local_folder(&folder_id, ctx) {
|
||||||
cloud_model.open_folder(folder_id, ctx);
|
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
|
||||||
});
|
repository.open_folder(folder_id, ctx);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cloud_model.update(ctx, |cloud_model, ctx| {
|
||||||
|
cloud_model.open_folder(folder_id, ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// If location is the trash, then the above move_[object]_to_location call already trashed the object
|
// If location is the trash, then the above move_[object]_to_location call already trashed the object
|
||||||
CloudObjectLocation::Trash => {}
|
CloudObjectLocation::Trash => {}
|
||||||
@@ -3508,9 +3520,15 @@ impl DriveIndex {
|
|||||||
if !new_name.is_empty() {
|
if !new_name.is_empty() {
|
||||||
self.reset_menus(ctx);
|
self.reset_menus(ctx);
|
||||||
|
|
||||||
UpdateManager::handle(ctx).update(ctx, move |update_manager, ctx| {
|
if Self::is_local_folder(&folder_id, ctx) {
|
||||||
update_manager.rename_folder(folder_id, new_name, ctx);
|
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
|
||||||
});
|
repository.update_folder_name(folder_id, new_name, ctx);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
UpdateManager::handle(ctx).update(ctx, move |update_manager, ctx| {
|
||||||
|
update_manager.rename_folder(folder_id, new_name, ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
self.cloud_object_naming_dialog.close(ctx);
|
self.cloud_object_naming_dialog.close(ctx);
|
||||||
ctx.notify();
|
ctx.notify();
|
||||||
@@ -3542,6 +3560,11 @@ impl DriveIndex {
|
|||||||
repository.set_env_var_collection_trashed(id, true, ctx);
|
repository.set_env_var_collection_trashed(id, true, ctx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
CloudObjectTypeAndId::Folder(id) if Self::is_local_folder(&id, ctx) => {
|
||||||
|
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
|
||||||
|
repository.set_folder_trashed(id, true, ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
CloudObjectTypeAndId::Folder(_) | CloudObjectTypeAndId::GenericStringObject { .. } => {
|
CloudObjectTypeAndId::Folder(_) | CloudObjectTypeAndId::GenericStringObject { .. } => {
|
||||||
UpdateManager::handle(ctx).update(ctx, move |update_manager, ctx| {
|
UpdateManager::handle(ctx).update(ctx, move |update_manager, ctx| {
|
||||||
update_manager.trash_object(cloud_object_type_and_id, ctx);
|
update_manager.trash_object(cloud_object_type_and_id, ctx);
|
||||||
@@ -3585,6 +3608,14 @@ impl DriveIndex {
|
|||||||
ctx.notify();
|
ctx.notify();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
CloudObjectTypeAndId::Folder(id) if Self::is_local_folder(id, ctx) => {
|
||||||
|
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
|
||||||
|
repository.set_folder_trashed(*id, false, ctx);
|
||||||
|
});
|
||||||
|
self.reset_menus(ctx);
|
||||||
|
ctx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
CloudObjectTypeAndId::Folder(_) | CloudObjectTypeAndId::GenericStringObject { .. } => {}
|
CloudObjectTypeAndId::Folder(_) | CloudObjectTypeAndId::GenericStringObject { .. } => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3736,6 +3767,11 @@ impl DriveIndex {
|
|||||||
repository.delete_env_var_collection(*id, ctx);
|
repository.delete_env_var_collection(*id, ctx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
CloudObjectTypeAndId::Folder(id) if Self::is_local_folder(id, ctx) => {
|
||||||
|
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
|
||||||
|
repository.delete_folder(*id, ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
CloudObjectTypeAndId::Folder(_) | CloudObjectTypeAndId::GenericStringObject { .. } => {
|
CloudObjectTypeAndId::Folder(_) | CloudObjectTypeAndId::GenericStringObject { .. } => {
|
||||||
UpdateManager::handle(ctx).update(ctx, move |update_manager, ctx| {
|
UpdateManager::handle(ctx).update(ctx, move |update_manager, ctx| {
|
||||||
update_manager.delete_object_by_user(*cloud_object_type_and_id, ctx);
|
update_manager.delete_object_by_user(*cloud_object_type_and_id, ctx);
|
||||||
@@ -5088,14 +5124,35 @@ impl DriveIndex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
CloudObjectTypeAndId::Folder(id) => {
|
CloudObjectTypeAndId::Folder(id) => {
|
||||||
CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| match key {
|
if Self::is_local_folder(id, ctx) {
|
||||||
DriveIndexAction::EnterKey => {
|
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
|
||||||
cloud_model.toggle_folder_open(*id, ctx);
|
match key {
|
||||||
}
|
DriveIndexAction::EnterKey => {
|
||||||
DriveIndexAction::LeftArrowKey => cloud_model.close_folder(*id, ctx),
|
repository.toggle_folder_open(*id, ctx);
|
||||||
DriveIndexAction::RightArrowKey => cloud_model.open_folder(*id, ctx),
|
}
|
||||||
_ => {}
|
DriveIndexAction::LeftArrowKey => {
|
||||||
});
|
repository.close_folder(*id, ctx)
|
||||||
|
}
|
||||||
|
DriveIndexAction::RightArrowKey => {
|
||||||
|
repository.open_folder(*id, ctx)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| match key {
|
||||||
|
DriveIndexAction::EnterKey => {
|
||||||
|
cloud_model.toggle_folder_open(*id, ctx);
|
||||||
|
}
|
||||||
|
DriveIndexAction::LeftArrowKey => {
|
||||||
|
cloud_model.close_folder(*id, ctx)
|
||||||
|
}
|
||||||
|
DriveIndexAction::RightArrowKey => {
|
||||||
|
cloud_model.open_folder(*id, ctx)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
CloudObjectTypeAndId::GenericStringObject { object_type, id: _ } => {
|
CloudObjectTypeAndId::GenericStringObject { object_type, id: _ } => {
|
||||||
if let GenericStringObjectFormat::Json(JsonObjectType::EnvVarCollection) =
|
if let GenericStringObjectFormat::Json(JsonObjectType::EnvVarCollection) =
|
||||||
@@ -5556,14 +5613,36 @@ impl TypedActionView for DriveIndex {
|
|||||||
ctx,
|
ctx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| {
|
if Self::is_local_folder(id, ctx) {
|
||||||
cloud_model.toggle_folder_open(*id, ctx);
|
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
|
||||||
});
|
repository.toggle_folder_open(*id, ctx);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| {
|
||||||
|
cloud_model.toggle_folder_open(*id, ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DriveIndexAction::CollapseAllInLocation(location) => {
|
DriveIndexAction::CollapseAllInLocation(location) => {
|
||||||
CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| {
|
if let CloudObjectLocation::Folder(folder_id) = location {
|
||||||
cloud_model.collapse_all_in_location(*location, self.index_variant, ctx);
|
if Self::is_local_folder(folder_id, ctx) {
|
||||||
});
|
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
|
||||||
|
repository.collapse_local_folders_in_location(*location, ctx);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| {
|
||||||
|
cloud_model.collapse_all_in_location(
|
||||||
|
*location,
|
||||||
|
self.index_variant,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| {
|
||||||
|
cloud_model.collapse_all_in_location(*location, self.index_variant, ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DriveIndexAction::TrashObject {
|
DriveIndexAction::TrashObject {
|
||||||
cloud_object_type_and_id,
|
cloud_object_type_and_id,
|
||||||
|
|||||||
+22
-11
@@ -178,17 +178,28 @@ impl DrivePanel {
|
|||||||
} => match Self::new_object_owner(*space, initial_folder_id.as_ref(), ctx) {
|
} => match Self::new_object_owner(*space, initial_folder_id.as_ref(), ctx) {
|
||||||
Some(owner) => {
|
Some(owner) => {
|
||||||
let client_id = ClientId::default();
|
let client_id = ClientId::default();
|
||||||
UpdateManager::handle(ctx).update(ctx, |update_manager, ctx| {
|
if owner == local_owner() {
|
||||||
update_manager.create_folder(
|
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
|
||||||
title.clone(),
|
repository.create_folder_with_id(
|
||||||
owner,
|
SyncId::ClientId(client_id),
|
||||||
client_id,
|
title.clone(),
|
||||||
*initial_folder_id,
|
*initial_folder_id,
|
||||||
true,
|
ctx,
|
||||||
InitiatedBy::User,
|
);
|
||||||
ctx,
|
});
|
||||||
);
|
} else {
|
||||||
});
|
UpdateManager::handle(ctx).update(ctx, |update_manager, ctx| {
|
||||||
|
update_manager.create_folder(
|
||||||
|
title.clone(),
|
||||||
|
owner,
|
||||||
|
client_id,
|
||||||
|
*initial_folder_id,
|
||||||
|
true,
|
||||||
|
InitiatedBy::User,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::error!("Cannot identify a folder owner from {space:?}");
|
log::error!("Cannot identify a folder owner from {space:?}");
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use galaxy_core::features::FeatureFlag;
|
|
||||||
use settings::macros::define_settings_group;
|
use settings::macros::define_settings_group;
|
||||||
use settings::{RespectUserSyncSetting, Setting, SupportedPlatforms, SyncToCloud};
|
use settings::{RespectUserSyncSetting, Setting, SupportedPlatforms, SyncToCloud};
|
||||||
|
|
||||||
@@ -37,14 +36,8 @@ define_settings_group!(WarpDriveSettings, settings: [
|
|||||||
|
|
||||||
impl WarpDriveSettings {
|
impl WarpDriveSettings {
|
||||||
/// Returns whether Warp Drive should be considered enabled.
|
/// Returns whether Warp Drive should be considered enabled.
|
||||||
/// Returns `false` when the user is anonymous or fully logged out,
|
|
||||||
/// regardless of the user setting.
|
|
||||||
pub fn is_warp_drive_enabled(app: &galaxyui::AppContext) -> bool {
|
pub fn is_warp_drive_enabled(app: &galaxyui::AppContext) -> bool {
|
||||||
use galaxyui::SingletonEntity as _;
|
use galaxyui::SingletonEntity as _;
|
||||||
let is_anonymous_or_logged_out = FeatureFlag::SkipFirebaseAnonymousUser.is_enabled()
|
*Self::as_ref(app).enable_warp_drive
|
||||||
&& crate::auth::AuthStateProvider::as_ref(app)
|
|
||||||
.get()
|
|
||||||
.is_anonymous_or_logged_out();
|
|
||||||
*Self::as_ref(app).enable_warp_drive && !is_anonymous_or_logged_out
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,16 +15,18 @@ use crate::auth::UserUid;
|
|||||||
use crate::cloud_object::model::generic_string_model::GenericStringObjectId;
|
use crate::cloud_object::model::generic_string_model::GenericStringObjectId;
|
||||||
use crate::cloud_object::model::persistence::{CloudModel, CloudModelEvent};
|
use crate::cloud_object::model::persistence::{CloudModel, CloudModelEvent};
|
||||||
use crate::cloud_object::{
|
use crate::cloud_object::{
|
||||||
CloudObject, CloudObjectMetadata, CloudObjectPermissions, CloudObjectStatuses,
|
CloudObject, CloudObjectLocation, CloudObjectMetadata, CloudObjectPermissions,
|
||||||
CloudObjectSyncStatus, GenericCloudObject, GenericStringObjectFormat, JsonObjectType,
|
CloudObjectStatuses, CloudObjectSyncStatus, GenericCloudObject, GenericStringObjectFormat,
|
||||||
ObjectIdType, Owner, Revision,
|
JsonObjectType, ObjectIdType, Owner, Revision, Space,
|
||||||
};
|
};
|
||||||
|
use crate::drive::folders::{CloudFolder, CloudFolderModel};
|
||||||
use crate::drive::CloudObjectTypeAndId;
|
use crate::drive::CloudObjectTypeAndId;
|
||||||
use crate::env_vars::{CloudEnvVarCollection, CloudEnvVarCollectionModel, EnvVarCollection};
|
use crate::env_vars::{CloudEnvVarCollection, CloudEnvVarCollectionModel, EnvVarCollection};
|
||||||
use crate::notebooks::{CloudNotebook, CloudNotebookModel};
|
use crate::notebooks::{CloudNotebook, CloudNotebookModel};
|
||||||
use crate::persistence::ModelEvent;
|
use crate::persistence::ModelEvent;
|
||||||
use crate::server::ids::{ClientId, SyncId};
|
use crate::server::ids::{ClientId, SyncId};
|
||||||
use crate::workflows::workflow::Workflow;
|
use crate::workflows::workflow::Workflow;
|
||||||
|
use crate::workflows::workflow_enum::{CloudWorkflowEnum, CloudWorkflowEnumModel, WorkflowEnum};
|
||||||
use crate::workflows::{CloudWorkflow, CloudWorkflowModel};
|
use crate::workflows::{CloudWorkflow, CloudWorkflowModel};
|
||||||
|
|
||||||
const LOCAL_OWNER_ID: &str = "local-galaxy-user";
|
const LOCAL_OWNER_ID: &str = "local-galaxy-user";
|
||||||
@@ -213,6 +215,140 @@ impl LocalObjectRepository {
|
|||||||
CloudModel::as_ref(app).get_notebook(id).cloned()
|
CloudModel::as_ref(app).get_notebook(id).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn folder(&self, id: &SyncId, app: &AppContext) -> Option<CloudFolder> {
|
||||||
|
CloudModel::as_ref(app).get_folder(id).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_folder_with_id(
|
||||||
|
&mut self,
|
||||||
|
id: SyncId,
|
||||||
|
name: String,
|
||||||
|
parent_folder_id: Option<SyncId>,
|
||||||
|
ctx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
self.upsert_folder(
|
||||||
|
new_local_folder(id, parent_folder_id, CloudFolderModel::new(&name, false)),
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_folder_name(
|
||||||
|
&mut self,
|
||||||
|
id: SyncId,
|
||||||
|
name: String,
|
||||||
|
ctx: &mut ModelContext<Self>,
|
||||||
|
) -> bool {
|
||||||
|
let Some(mut folder) = self.folder(&id, ctx) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
folder.set_model(CloudFolderModel {
|
||||||
|
name,
|
||||||
|
is_open: folder.model().is_open,
|
||||||
|
is_warp_pack: folder.model().is_warp_pack,
|
||||||
|
});
|
||||||
|
set_locally_saved_metadata(&mut folder.metadata);
|
||||||
|
self.upsert_folder(folder, ctx);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_folder_trashed(
|
||||||
|
&mut self,
|
||||||
|
id: SyncId,
|
||||||
|
trashed: bool,
|
||||||
|
ctx: &mut ModelContext<Self>,
|
||||||
|
) -> bool {
|
||||||
|
let Some(mut folder) = self.folder(&id, ctx) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
folder.metadata.trashed_ts = trashed.then(|| ServerTimestamp::new(Utc::now()));
|
||||||
|
set_locally_saved_metadata(&mut folder.metadata);
|
||||||
|
self.upsert_folder(folder, ctx);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_folder(&mut self, id: SyncId, ctx: &mut ModelContext<Self>) -> bool {
|
||||||
|
if self.folder(&id, ctx).is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ids = CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| {
|
||||||
|
cloud_model.delete_object_and_descendants(id.uid(), ctx)
|
||||||
|
});
|
||||||
|
if ids.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.save(ModelEvent::DeleteObjects { ids });
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_folder(&mut self, id: SyncId, ctx: &mut ModelContext<Self>) {
|
||||||
|
self.set_folder_open_state(id, true, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_folder(&mut self, id: SyncId, ctx: &mut ModelContext<Self>) {
|
||||||
|
self.set_folder_open_state(id, false, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_folder_open(&mut self, id: SyncId, ctx: &mut ModelContext<Self>) {
|
||||||
|
let Some(folder) = self.folder(&id, ctx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.set_folder_open_state(id, !folder.model().is_open, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collapse_local_folders_in_location(
|
||||||
|
&mut self,
|
||||||
|
location: CloudObjectLocation,
|
||||||
|
ctx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
let folder_ids = {
|
||||||
|
let cloud_model = CloudModel::as_ref(ctx);
|
||||||
|
cloud_model
|
||||||
|
.get_all_active_and_inactive_folders()
|
||||||
|
.filter(|folder| folder.permissions.owner == local_owner())
|
||||||
|
.filter(|folder| match location {
|
||||||
|
CloudObjectLocation::Folder(parent_id) => {
|
||||||
|
if folder.id == parent_id {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ancestor_id = folder.metadata.folder_id;
|
||||||
|
while let Some(id) = ancestor_id {
|
||||||
|
if id == parent_id {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ancestor_id = cloud_model
|
||||||
|
.get_folder(&id)
|
||||||
|
.and_then(|ancestor| ancestor.metadata.folder_id);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
CloudObjectLocation::Space(Space::Personal) => true,
|
||||||
|
CloudObjectLocation::Space(Space::Shared)
|
||||||
|
| CloudObjectLocation::Space(Space::Team { .. })
|
||||||
|
| CloudObjectLocation::Trash => false,
|
||||||
|
})
|
||||||
|
.map(|folder| folder.id)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
for folder_id in folder_ids {
|
||||||
|
self.close_folder(folder_id, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_folder_open_state(&self, id: SyncId, is_open: bool, ctx: &mut ModelContext<Self>) {
|
||||||
|
let Some(mut folder) = self.folder(&id, ctx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
folder.set_model(CloudFolderModel {
|
||||||
|
name: folder.model().name.clone(),
|
||||||
|
is_open,
|
||||||
|
is_warp_pack: folder.model().is_warp_pack,
|
||||||
|
});
|
||||||
|
self.upsert_folder(folder, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_notebook_with_id(
|
pub fn create_notebook_with_id(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: SyncId,
|
id: SyncId,
|
||||||
@@ -480,6 +616,23 @@ impl LocalObjectRepository {
|
|||||||
self.upsert_workflow(new_local_workflow(id, folder_id, workflow), ctx);
|
self.upsert_workflow(new_local_workflow(id, folder_id, workflow), ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_workflow_enum_with_id(
|
||||||
|
&mut self,
|
||||||
|
id: SyncId,
|
||||||
|
workflow_enum: WorkflowEnum,
|
||||||
|
ctx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
self.upsert_workflow_enum(
|
||||||
|
GenericCloudObject::new(
|
||||||
|
id,
|
||||||
|
CloudWorkflowEnumModel::new(workflow_enum),
|
||||||
|
locally_saved_metadata(None),
|
||||||
|
local_permissions(),
|
||||||
|
),
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update_workflow(
|
pub fn update_workflow(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: SyncId,
|
id: SyncId,
|
||||||
@@ -591,6 +744,13 @@ impl LocalObjectRepository {
|
|||||||
self.save(ModelEvent::UpsertNotebook { notebook });
|
self.save(ModelEvent::UpsertNotebook { notebook });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn upsert_folder(&self, folder: CloudFolder, ctx: &mut ModelContext<Self>) {
|
||||||
|
CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| {
|
||||||
|
cloud_model.upsert_local_object(folder.clone(), ctx);
|
||||||
|
});
|
||||||
|
self.save(ModelEvent::UpsertFolder { folder });
|
||||||
|
}
|
||||||
|
|
||||||
fn upsert_workflow(&self, workflow: CloudWorkflow, ctx: &mut ModelContext<Self>) {
|
fn upsert_workflow(&self, workflow: CloudWorkflow, ctx: &mut ModelContext<Self>) {
|
||||||
CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| {
|
CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| {
|
||||||
cloud_model.upsert_local_object(workflow.clone(), ctx);
|
cloud_model.upsert_local_object(workflow.clone(), ctx);
|
||||||
@@ -598,6 +758,15 @@ impl LocalObjectRepository {
|
|||||||
self.save(ModelEvent::UpsertWorkflow { workflow });
|
self.save(ModelEvent::UpsertWorkflow { workflow });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn upsert_workflow_enum(&self, workflow_enum: CloudWorkflowEnum, ctx: &mut ModelContext<Self>) {
|
||||||
|
CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| {
|
||||||
|
cloud_model.upsert_local_object(workflow_enum.clone(), ctx);
|
||||||
|
});
|
||||||
|
self.save(ModelEvent::UpsertGenericStringObject {
|
||||||
|
object: Box::new(workflow_enum),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn delete_local_object(
|
fn delete_local_object(
|
||||||
&self,
|
&self,
|
||||||
id: SyncId,
|
id: SyncId,
|
||||||
@@ -658,6 +827,19 @@ pub(crate) fn new_local_notebook(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new_local_folder(
|
||||||
|
id: SyncId,
|
||||||
|
folder_id: Option<SyncId>,
|
||||||
|
folder: CloudFolderModel,
|
||||||
|
) -> CloudFolder {
|
||||||
|
CloudFolder::new(
|
||||||
|
id,
|
||||||
|
folder,
|
||||||
|
locally_saved_metadata(folder_id),
|
||||||
|
local_permissions(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn new_local_workflow(
|
pub(crate) fn new_local_workflow(
|
||||||
id: SyncId,
|
id: SyncId,
|
||||||
folder_id: Option<SyncId>,
|
folder_id: Option<SyncId>,
|
||||||
|
|||||||
@@ -172,6 +172,71 @@ fn create_update_and_delete_notebook_are_local_and_persisted() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_update_open_trash_and_delete_folder_are_local_and_persisted() {
|
||||||
|
App::test((), |mut app| async move {
|
||||||
|
let receiver = initialize_app(&mut app);
|
||||||
|
let repository = LocalObjectRepository::handle(&app);
|
||||||
|
let parent_id = SyncId::ClientId(ClientId::new());
|
||||||
|
let child_id = SyncId::ClientId(ClientId::new());
|
||||||
|
|
||||||
|
repository.update(&mut app, |repository, ctx| {
|
||||||
|
repository.create_folder_with_id(parent_id, "Projects".to_string(), None, ctx);
|
||||||
|
});
|
||||||
|
let ModelEvent::UpsertFolder { folder } = receiver.recv().unwrap() else {
|
||||||
|
panic!("expected a local folder upsert");
|
||||||
|
};
|
||||||
|
assert_eq!(folder.id, parent_id);
|
||||||
|
assert_eq!(folder.model().name, "Projects");
|
||||||
|
assert!(!folder.model().is_open);
|
||||||
|
|
||||||
|
repository.update(&mut app, |repository, ctx| {
|
||||||
|
repository.create_folder_with_id(child_id, "Rust".to_string(), Some(parent_id), ctx);
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
matches!(receiver.recv().unwrap(), ModelEvent::UpsertFolder { folder } if folder.id == child_id && folder.metadata.folder_id == Some(parent_id))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(repository.update(&mut app, |repository, ctx| {
|
||||||
|
repository.update_folder_name(parent_id, "Projects 2026".to_string(), ctx)
|
||||||
|
}));
|
||||||
|
assert!(
|
||||||
|
matches!(receiver.recv().unwrap(), ModelEvent::UpsertFolder { folder } if folder.model().name == "Projects 2026")
|
||||||
|
);
|
||||||
|
|
||||||
|
repository.update(&mut app, |repository, ctx| {
|
||||||
|
repository.open_folder(parent_id, ctx);
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
matches!(receiver.recv().unwrap(), ModelEvent::UpsertFolder { folder } if folder.model().is_open)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(repository.update(&mut app, |repository, ctx| {
|
||||||
|
repository.set_folder_trashed(parent_id, true, ctx)
|
||||||
|
}));
|
||||||
|
assert!(
|
||||||
|
matches!(receiver.recv().unwrap(), ModelEvent::UpsertFolder { folder } if folder.metadata.trashed_ts.is_some())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(repository.update(&mut app, |repository, ctx| {
|
||||||
|
repository.set_folder_trashed(parent_id, false, ctx)
|
||||||
|
}));
|
||||||
|
assert!(
|
||||||
|
matches!(receiver.recv().unwrap(), ModelEvent::UpsertFolder { folder } if folder.metadata.trashed_ts.is_none())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(repository.update(&mut app, |repository, ctx| {
|
||||||
|
repository.delete_folder(parent_id, ctx)
|
||||||
|
}));
|
||||||
|
assert!(matches!(
|
||||||
|
receiver.recv().unwrap(),
|
||||||
|
ModelEvent::DeleteObjects { ids }
|
||||||
|
if ids.contains(&(parent_id, ObjectIdType::Folder))
|
||||||
|
&& ids.contains(&(child_id, ObjectIdType::Folder))
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_update_and_delete_workflow_are_local_and_persisted() {
|
fn create_update_and_delete_workflow_are_local_and_persisted() {
|
||||||
App::test((), |mut app| async move {
|
App::test((), |mut app| async move {
|
||||||
|
|||||||
@@ -926,6 +926,13 @@ pub enum OpenAIProviderKind {
|
|||||||
OpenAICompatible,
|
OpenAICompatible,
|
||||||
/// The ChatGPT subscription backend, authenticated with ChatGPT OAuth.
|
/// The ChatGPT subscription backend, authenticated with ChatGPT OAuth.
|
||||||
ChatGPTSubscription,
|
ChatGPTSubscription,
|
||||||
|
/// Anthropic's native Messages API.
|
||||||
|
Anthropic,
|
||||||
|
/// Google's Gemini API.
|
||||||
|
Gemini,
|
||||||
|
/// Google's Gemini models hosted through Vertex AI.
|
||||||
|
#[serde(rename = "vertex_ai", alias = "vertex_a_i")]
|
||||||
|
VertexAI,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for a single OpenAI-compatible provider endpoint.
|
/// Configuration for a single OpenAI-compatible provider endpoint.
|
||||||
@@ -951,6 +958,12 @@ pub struct OpenAIProviderConfig {
|
|||||||
#[schemars(description = "API key for this endpoint (optional if the proxy handles auth).")]
|
#[schemars(description = "API key for this endpoint (optional if the proxy handles auth).")]
|
||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[schemars(description = "Google Cloud project ID for Vertex AI providers.")]
|
||||||
|
pub project_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[schemars(description = "Google Cloud location for Vertex AI providers.")]
|
||||||
|
pub location: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
#[schemars(description = "Models available from this provider.")]
|
#[schemars(description = "Models available from this provider.")]
|
||||||
pub models: Vec<OpenAIModelConfig>,
|
pub models: Vec<OpenAIModelConfig>,
|
||||||
}
|
}
|
||||||
@@ -1021,6 +1034,8 @@ pub(crate) fn default_chatgpt_provider() -> OpenAIProviderConfig {
|
|||||||
name: "ChatGPT Subscription".to_string(),
|
name: "ChatGPT Subscription".to_string(),
|
||||||
base_url: String::new(),
|
base_url: String::new(),
|
||||||
api_key: None,
|
api_key: None,
|
||||||
|
project_id: None,
|
||||||
|
location: None,
|
||||||
models: default_chatgpt_models(),
|
models: default_chatgpt_models(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1035,6 +1050,8 @@ fn default_openai_providers() -> Vec<OpenAIProviderConfig> {
|
|||||||
// Credentials are deliberately never committed. Set this locally in
|
// Credentials are deliberately never committed. Set this locally in
|
||||||
// ~/.galaxy/settings.toml before sending a request.
|
// ~/.galaxy/settings.toml before sending a request.
|
||||||
api_key: None,
|
api_key: None,
|
||||||
|
project_id: None,
|
||||||
|
location: None,
|
||||||
models: vec![OpenAIModelConfig {
|
models: vec![OpenAIModelConfig {
|
||||||
model_id: INITIAL_RIG_MODEL_ID.to_string(),
|
model_id: INITIAL_RIG_MODEL_ID.to_string(),
|
||||||
display_name: "Codex GPT-5.6 SOL (xhigh)".to_string(),
|
display_name: "Codex GPT-5.6 SOL (xhigh)".to_string(),
|
||||||
|
|||||||
@@ -423,6 +423,34 @@ fn initial_litellm_provider_maps_codex_model_to_rig_without_a_committed_key() {
|
|||||||
assert!(instant.reasoning_efforts.is_empty());
|
assert!(instant.reasoning_efforts.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_provider_settings_roundtrip_with_vertex_configuration() {
|
||||||
|
let provider: OpenAIProviderConfig = serde_json::from_value(serde_json::json!({
|
||||||
|
"kind": "vertex_ai",
|
||||||
|
"enabled": true,
|
||||||
|
"name": "Vertex production",
|
||||||
|
"base_url": "",
|
||||||
|
"project_id": "galaxy-project",
|
||||||
|
"location": "us-central1",
|
||||||
|
"models": []
|
||||||
|
}))
|
||||||
|
.expect("Vertex provider settings should deserialize");
|
||||||
|
|
||||||
|
assert_eq!(provider.kind, OpenAIProviderKind::VertexAI);
|
||||||
|
assert_eq!(provider.project_id.as_deref(), Some("galaxy-project"));
|
||||||
|
assert_eq!(provider.location.as_deref(), Some("us-central1"));
|
||||||
|
|
||||||
|
let legacy: OpenAIProviderConfig = serde_json::from_value(serde_json::json!({
|
||||||
|
"name": "Legacy provider",
|
||||||
|
"base_url": "http://localhost:4000/v1",
|
||||||
|
"models": []
|
||||||
|
}))
|
||||||
|
.expect("Legacy provider settings should remain compatible");
|
||||||
|
assert_eq!(legacy.kind, OpenAIProviderKind::OpenAICompatible);
|
||||||
|
assert_eq!(legacy.project_id, None);
|
||||||
|
assert_eq!(legacy.location, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn codex_litellm_model_infers_missing_system_message_capability() {
|
fn codex_litellm_model_infers_missing_system_message_capability() {
|
||||||
let mut model = default_openai_providers().remove(0).models.remove(0);
|
let mut model = default_openai_providers().remove(0).models.remove(0);
|
||||||
|
|||||||
@@ -7564,13 +7564,13 @@ impl SettingsWidget for ModelsOverviewWidget {
|
|||||||
.with_spacing(8.)
|
.with_spacing(8.)
|
||||||
.with_child(build_sub_header(appearance, "Models", None).finish())
|
.with_child(build_sub_header(appearance, "Models", None).finish())
|
||||||
.with_child(render_ai_setting_description(
|
.with_child(render_ai_setting_description(
|
||||||
"Configure Galaxy's direct model providers and agent runtimes in one place. OpenAI-compatible endpoints and Bedrock models run through Rig. ACP coding agents use the same Galaxy runtime boundary while retaining their own model, login, session, and tool loop.",
|
"Configure Galaxy's direct model providers and agent runtimes in one place. OpenAI-compatible, Anthropic, Gemini, Vertex AI, and Bedrock models run through Rig. ACP coding agents use the same Galaxy runtime boundary while retaining their own model, login, session, and tool loop.",
|
||||||
true,
|
true,
|
||||||
app,
|
app,
|
||||||
))
|
))
|
||||||
.with_child(render_ai_setting_description(
|
.with_child(render_ai_setting_description(
|
||||||
format!(
|
format!(
|
||||||
"{endpoint_count} OpenAI-compatible provider(s) with {endpoint_model_count} model(s); {bedrock_model_count} Bedrock model(s); {agent_runtime_count} enabled agent runtime(s)."
|
"{endpoint_count} configured provider(s) with {endpoint_model_count} model(s); {bedrock_model_count} Bedrock model(s); {agent_runtime_count} enabled agent runtime(s)."
|
||||||
),
|
),
|
||||||
true,
|
true,
|
||||||
app,
|
app,
|
||||||
@@ -7680,6 +7680,9 @@ impl OpenAIProviderSettingsWidget {
|
|||||||
match provider.kind {
|
match provider.kind {
|
||||||
OpenAIProviderKind::OpenAICompatible => "OpenAI-compatible API",
|
OpenAIProviderKind::OpenAICompatible => "OpenAI-compatible API",
|
||||||
OpenAIProviderKind::ChatGPTSubscription => "ChatGPT subscription",
|
OpenAIProviderKind::ChatGPTSubscription => "ChatGPT subscription",
|
||||||
|
OpenAIProviderKind::Anthropic => "Anthropic",
|
||||||
|
OpenAIProviderKind::Gemini => "Google Gemini",
|
||||||
|
OpenAIProviderKind::VertexAI => "Google Vertex AI",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7905,33 +7908,37 @@ impl SettingsWidget for OpenAIProviderSettingsWidget {
|
|||||||
app,
|
app,
|
||||||
));
|
));
|
||||||
column.add_child(render_ai_setting_description(
|
column.add_child(render_ai_setting_description(
|
||||||
"Connect a ChatGPT subscription, OpenAI-compatible endpoint, AWS Bedrock account, or ACP agent runtime. Each provider can be enabled independently.",
|
"Connect a ChatGPT subscription, OpenAI-compatible endpoint, Anthropic, Gemini, Vertex AI, AWS Bedrock account, or ACP agent runtime. Each provider can be enabled independently.",
|
||||||
true,
|
true,
|
||||||
app,
|
app,
|
||||||
));
|
));
|
||||||
|
|
||||||
column.add_child(self.render_builtin_provider_card(
|
if !settings.bedrock_models.value().is_empty() {
|
||||||
settings.bedrock_connection_name.value().as_str(),
|
|
||||||
"Use AWS credentials to access Bedrock foundation models directly.",
|
|
||||||
*settings.bedrock_enabled.value(),
|
|
||||||
self.bedrock_enabled_toggle.clone(),
|
|
||||||
AISettingsPageAction::ToggleBedrockEnabled,
|
|
||||||
&self.bedrock_edit_button,
|
|
||||||
&self.bedrock_remove_button,
|
|
||||||
appearance,
|
|
||||||
));
|
|
||||||
if cfg!(unix) && FeatureFlag::AgentClientProtocol.is_enabled() {
|
|
||||||
column.add_child(self.render_builtin_provider_card(
|
column.add_child(self.render_builtin_provider_card(
|
||||||
settings.acp_connection_name.value().as_str(),
|
settings.bedrock_connection_name.value().as_str(),
|
||||||
"Use a local session-oriented agent that owns its model and authentication.",
|
"Use AWS credentials to access Bedrock foundation models directly.",
|
||||||
*settings.acp_enabled.value(),
|
*settings.bedrock_enabled.value(),
|
||||||
self.acp_enabled_toggle.clone(),
|
self.bedrock_enabled_toggle.clone(),
|
||||||
AISettingsPageAction::ToggleAcpEnabled,
|
AISettingsPageAction::ToggleBedrockEnabled,
|
||||||
&self.acp_edit_button,
|
&self.bedrock_edit_button,
|
||||||
&self.acp_remove_button,
|
&self.bedrock_remove_button,
|
||||||
appearance,
|
appearance,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if cfg!(unix) && FeatureFlag::AgentClientProtocol.is_enabled() {
|
||||||
|
if *settings.acp_enabled.value() {
|
||||||
|
column.add_child(self.render_builtin_provider_card(
|
||||||
|
settings.acp_connection_name.value().as_str(),
|
||||||
|
"Use a local session-oriented agent that owns its model and authentication.",
|
||||||
|
*settings.acp_enabled.value(),
|
||||||
|
self.acp_enabled_toggle.clone(),
|
||||||
|
AISettingsPageAction::ToggleAcpEnabled,
|
||||||
|
&self.acp_edit_button,
|
||||||
|
&self.acp_remove_button,
|
||||||
|
appearance,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if providers.is_empty() {
|
if providers.is_empty() {
|
||||||
column.add_child(render_ai_setting_description(
|
column.add_child(render_ai_setting_description(
|
||||||
|
|||||||
@@ -1167,9 +1167,6 @@ impl SettingsView {
|
|||||||
// Warp Drive page
|
// Warp Drive page
|
||||||
let warp_drive_page_handle =
|
let warp_drive_page_handle =
|
||||||
ctx.add_typed_action_view(warp_drive_page::WarpDriveSettingsPageView::new);
|
ctx.add_typed_action_view(warp_drive_page::WarpDriveSettingsPageView::new);
|
||||||
ctx.subscribe_to_view(&warp_drive_page_handle, |me, _, event, ctx| {
|
|
||||||
me.handle_warp_drive_page_event(event, ctx);
|
|
||||||
});
|
|
||||||
|
|
||||||
let platform_page_handle = ctx.add_typed_action_view(platform_page::PlatformPageView::new);
|
let platform_page_handle = ctx.add_typed_action_view(platform_page::PlatformPageView::new);
|
||||||
ctx.subscribe_to_view(&platform_page_handle, |me, _, event, ctx| {
|
ctx.subscribe_to_view(&platform_page_handle, |me, _, event, ctx| {
|
||||||
@@ -1738,18 +1735,6 @@ impl SettingsView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_warp_drive_page_event(
|
|
||||||
&mut self,
|
|
||||||
event: &warp_drive_page::WarpDriveSettingsPageEvent,
|
|
||||||
ctx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
warp_drive_page::WarpDriveSettingsPageEvent::SignUp => {
|
|
||||||
ctx.emit(SettingsViewEvent::SignupAnonymousUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_ai_page_event(&mut self, event: &AISettingsPageEvent, ctx: &mut ViewContext<Self>) {
|
fn handle_ai_page_event(&mut self, event: &AISettingsPageEvent, ctx: &mut ViewContext<Self>) {
|
||||||
match event {
|
match event {
|
||||||
AISettingsPageEvent::FocusModal => ctx.focus(&self.search_editor),
|
AISettingsPageEvent::FocusModal => ctx.focus(&self.search_editor),
|
||||||
|
|||||||
@@ -46,10 +46,51 @@ enum ProviderSetupStep {
|
|||||||
pub enum ProviderSetupProviderType {
|
pub enum ProviderSetupProviderType {
|
||||||
ChatGPTSubscription,
|
ChatGPTSubscription,
|
||||||
OpenAICompatible,
|
OpenAICompatible,
|
||||||
|
Anthropic,
|
||||||
|
Gemini,
|
||||||
|
VertexAI,
|
||||||
Bedrock,
|
Bedrock,
|
||||||
Acp,
|
Acp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PROVIDER_TYPE_OPTIONS: &[(ProviderSetupProviderType, &str, &str)] = &[
|
||||||
|
(
|
||||||
|
ProviderSetupProviderType::ChatGPTSubscription,
|
||||||
|
"ChatGPT subscription",
|
||||||
|
"Use your ChatGPT Plus or Pro subscription with native OAuth.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ProviderSetupProviderType::OpenAICompatible,
|
||||||
|
"OpenAI-compatible API",
|
||||||
|
"Connect LiteLLM, Ollama, vLLM, or another compatible endpoint.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ProviderSetupProviderType::Anthropic,
|
||||||
|
"Anthropic",
|
||||||
|
"Connect directly to Anthropic's native Messages API with an API key.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ProviderSetupProviderType::Gemini,
|
||||||
|
"Google Gemini",
|
||||||
|
"Connect directly to Google's Gemini API with an API key.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ProviderSetupProviderType::VertexAI,
|
||||||
|
"Google Vertex AI",
|
||||||
|
"Use Google Cloud Application Default Credentials for Vertex-hosted Gemini models.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ProviderSetupProviderType::Bedrock,
|
||||||
|
"AWS Bedrock",
|
||||||
|
"Use the AWS Bedrock credentials and model configuration already managed by Galaxy.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ProviderSetupProviderType::Acp,
|
||||||
|
"ACP agent runtime",
|
||||||
|
"Use a session-oriented ACP agent that owns its model and authentication.",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct BedrockProviderDraft {
|
pub struct BedrockProviderDraft {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -114,6 +155,8 @@ pub struct ProviderSetupModalBody {
|
|||||||
draft_name: String,
|
draft_name: String,
|
||||||
draft_base_url: String,
|
draft_base_url: String,
|
||||||
draft_api_key: Option<String>,
|
draft_api_key: Option<String>,
|
||||||
|
draft_project_id: String,
|
||||||
|
draft_location: String,
|
||||||
draft_models: Vec<OpenAIModelConfig>,
|
draft_models: Vec<OpenAIModelConfig>,
|
||||||
draft_bedrock: BedrockProviderDraft,
|
draft_bedrock: BedrockProviderDraft,
|
||||||
draft_acp: AcpProviderDraft,
|
draft_acp: AcpProviderDraft,
|
||||||
@@ -122,6 +165,8 @@ pub struct ProviderSetupModalBody {
|
|||||||
name_editor: ViewHandle<EditorView>,
|
name_editor: ViewHandle<EditorView>,
|
||||||
base_url_editor: ViewHandle<EditorView>,
|
base_url_editor: ViewHandle<EditorView>,
|
||||||
api_key_editor: ViewHandle<EditorView>,
|
api_key_editor: ViewHandle<EditorView>,
|
||||||
|
project_id_editor: ViewHandle<EditorView>,
|
||||||
|
location_editor: ViewHandle<EditorView>,
|
||||||
bedrock_profile_editor: ViewHandle<EditorView>,
|
bedrock_profile_editor: ViewHandle<EditorView>,
|
||||||
bedrock_region_editor: ViewHandle<EditorView>,
|
bedrock_region_editor: ViewHandle<EditorView>,
|
||||||
bedrock_refresh_command_editor: ViewHandle<EditorView>,
|
bedrock_refresh_command_editor: ViewHandle<EditorView>,
|
||||||
@@ -134,6 +179,7 @@ pub struct ProviderSetupModalBody {
|
|||||||
bedrock_cross_region_toggle: SwitchStateHandle,
|
bedrock_cross_region_toggle: SwitchStateHandle,
|
||||||
bedrock_auto_login_toggle: SwitchStateHandle,
|
bedrock_auto_login_toggle: SwitchStateHandle,
|
||||||
model_switches: Vec<SwitchStateHandle>,
|
model_switches: Vec<SwitchStateHandle>,
|
||||||
|
provider_type_scroll_state: ClippedScrollStateHandle,
|
||||||
models_scroll_state: ClippedScrollStateHandle,
|
models_scroll_state: ClippedScrollStateHandle,
|
||||||
back_button: ViewHandle<ActionButton>,
|
back_button: ViewHandle<ActionButton>,
|
||||||
cancel_button: ViewHandle<ActionButton>,
|
cancel_button: ViewHandle<ActionButton>,
|
||||||
@@ -142,35 +188,28 @@ pub struct ProviderSetupModalBody {
|
|||||||
|
|
||||||
impl ProviderSetupModalBody {
|
impl ProviderSetupModalBody {
|
||||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||||
let provider_type_buttons = [
|
let provider_type_buttons = PROVIDER_TYPE_OPTIONS
|
||||||
(
|
.iter()
|
||||||
ProviderSetupProviderType::ChatGPTSubscription,
|
.map(|(kind, label, _)| {
|
||||||
"ChatGPT subscription",
|
let kind = *kind;
|
||||||
),
|
let label = *label;
|
||||||
(
|
ctx.add_typed_action_view(move |_| {
|
||||||
ProviderSetupProviderType::OpenAICompatible,
|
ActionButton::new(label, NakedTheme)
|
||||||
"OpenAI-compatible API",
|
.with_full_width(true)
|
||||||
),
|
.on_click(move |ctx| {
|
||||||
(ProviderSetupProviderType::Bedrock, "AWS Bedrock"),
|
ctx.dispatch_typed_action(
|
||||||
(ProviderSetupProviderType::Acp, "ACP agent runtime"),
|
ProviderSetupModalBodyAction::SelectProvider(kind),
|
||||||
]
|
);
|
||||||
.into_iter()
|
})
|
||||||
.map(|(kind, label)| {
|
})
|
||||||
ctx.add_typed_action_view(move |_| {
|
|
||||||
ActionButton::new(label, NakedTheme)
|
|
||||||
.with_full_width(true)
|
|
||||||
.on_click(move |ctx| {
|
|
||||||
ctx.dispatch_typed_action(ProviderSetupModalBodyAction::SelectProvider(
|
|
||||||
kind,
|
|
||||||
));
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
.collect();
|
||||||
.collect();
|
|
||||||
|
|
||||||
let name_editor = Self::create_editor("Connection name", false, ctx);
|
let name_editor = Self::create_editor("Connection name", false, ctx);
|
||||||
let base_url_editor = Self::create_editor("https://api.example.com/v1", false, ctx);
|
let base_url_editor = Self::create_editor("https://api.example.com/v1", false, ctx);
|
||||||
let api_key_editor = Self::create_editor("sk-... (optional)", true, ctx);
|
let api_key_editor = Self::create_editor("sk-... (optional)", true, ctx);
|
||||||
|
let project_id_editor = Self::create_editor("my-google-cloud-project", false, ctx);
|
||||||
|
let location_editor = Self::create_editor("global", false, ctx);
|
||||||
let bedrock_profile_editor = Self::create_editor("default", false, ctx);
|
let bedrock_profile_editor = Self::create_editor("default", false, ctx);
|
||||||
let bedrock_region_editor = Self::create_editor("us-east-1", false, ctx);
|
let bedrock_region_editor = Self::create_editor("us-east-1", false, ctx);
|
||||||
let bedrock_refresh_command_editor = Self::create_editor("aws sso login", false, ctx);
|
let bedrock_refresh_command_editor = Self::create_editor("aws sso login", false, ctx);
|
||||||
@@ -218,6 +257,19 @@ impl ProviderSetupModalBody {
|
|||||||
ctx.notify();
|
ctx.notify();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
ctx.subscribe_to_view(&project_id_editor, |me, editor, event, ctx| {
|
||||||
|
if matches!(event, EditorEvent::Edited(_)) {
|
||||||
|
me.draft_project_id = editor.as_ref(ctx).buffer_text(ctx);
|
||||||
|
me.update_next_button(ctx);
|
||||||
|
ctx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctx.subscribe_to_view(&location_editor, |me, editor, event, ctx| {
|
||||||
|
if matches!(event, EditorEvent::Edited(_)) {
|
||||||
|
me.draft_location = editor.as_ref(ctx).buffer_text(ctx);
|
||||||
|
ctx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
for (editor, update) in [
|
for (editor, update) in [
|
||||||
(bedrock_profile_editor.clone(), 0),
|
(bedrock_profile_editor.clone(), 0),
|
||||||
(bedrock_region_editor.clone(), 1),
|
(bedrock_region_editor.clone(), 1),
|
||||||
@@ -286,6 +338,8 @@ impl ProviderSetupModalBody {
|
|||||||
draft_name: String::new(),
|
draft_name: String::new(),
|
||||||
draft_base_url: String::new(),
|
draft_base_url: String::new(),
|
||||||
draft_api_key: None,
|
draft_api_key: None,
|
||||||
|
draft_project_id: String::new(),
|
||||||
|
draft_location: "global".to_string(),
|
||||||
draft_models: Vec::new(),
|
draft_models: Vec::new(),
|
||||||
draft_bedrock: BedrockProviderDraft {
|
draft_bedrock: BedrockProviderDraft {
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
@@ -310,6 +364,8 @@ impl ProviderSetupModalBody {
|
|||||||
name_editor,
|
name_editor,
|
||||||
base_url_editor,
|
base_url_editor,
|
||||||
api_key_editor,
|
api_key_editor,
|
||||||
|
project_id_editor,
|
||||||
|
location_editor,
|
||||||
bedrock_profile_editor,
|
bedrock_profile_editor,
|
||||||
bedrock_region_editor,
|
bedrock_region_editor,
|
||||||
bedrock_refresh_command_editor,
|
bedrock_refresh_command_editor,
|
||||||
@@ -322,6 +378,7 @@ impl ProviderSetupModalBody {
|
|||||||
bedrock_cross_region_toggle: SwitchStateHandle::default(),
|
bedrock_cross_region_toggle: SwitchStateHandle::default(),
|
||||||
bedrock_auto_login_toggle: SwitchStateHandle::default(),
|
bedrock_auto_login_toggle: SwitchStateHandle::default(),
|
||||||
model_switches: Vec::new(),
|
model_switches: Vec::new(),
|
||||||
|
provider_type_scroll_state: ClippedScrollStateHandle::default(),
|
||||||
models_scroll_state: ClippedScrollStateHandle::default(),
|
models_scroll_state: ClippedScrollStateHandle::default(),
|
||||||
back_button,
|
back_button,
|
||||||
cancel_button,
|
cancel_button,
|
||||||
@@ -363,6 +420,8 @@ impl ProviderSetupModalBody {
|
|||||||
self.draft_name.clear();
|
self.draft_name.clear();
|
||||||
self.draft_base_url.clear();
|
self.draft_base_url.clear();
|
||||||
self.draft_api_key = None;
|
self.draft_api_key = None;
|
||||||
|
self.draft_project_id.clear();
|
||||||
|
self.draft_location = "global".to_string();
|
||||||
self.draft_models.clear();
|
self.draft_models.clear();
|
||||||
self.draft_bedrock = BedrockProviderDraft {
|
self.draft_bedrock = BedrockProviderDraft {
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
@@ -405,10 +464,15 @@ impl ProviderSetupModalBody {
|
|||||||
ProviderSetupProviderType::ChatGPTSubscription
|
ProviderSetupProviderType::ChatGPTSubscription
|
||||||
}
|
}
|
||||||
OpenAIProviderKind::OpenAICompatible => ProviderSetupProviderType::OpenAICompatible,
|
OpenAIProviderKind::OpenAICompatible => ProviderSetupProviderType::OpenAICompatible,
|
||||||
|
OpenAIProviderKind::Anthropic => ProviderSetupProviderType::Anthropic,
|
||||||
|
OpenAIProviderKind::Gemini => ProviderSetupProviderType::Gemini,
|
||||||
|
OpenAIProviderKind::VertexAI => ProviderSetupProviderType::VertexAI,
|
||||||
};
|
};
|
||||||
self.draft_name = provider.name;
|
self.draft_name = provider.name;
|
||||||
self.draft_base_url = provider.base_url;
|
self.draft_base_url = provider.base_url;
|
||||||
self.draft_api_key = provider.api_key;
|
self.draft_api_key = provider.api_key;
|
||||||
|
self.draft_project_id = provider.project_id.unwrap_or_default();
|
||||||
|
self.draft_location = provider.location.unwrap_or_else(|| "global".to_string());
|
||||||
self.draft_models = provider.models;
|
self.draft_models = provider.models;
|
||||||
self.discovery_state = DiscoveryState::Idle;
|
self.discovery_state = DiscoveryState::Idle;
|
||||||
self.sync_editors(ctx);
|
self.sync_editors(ctx);
|
||||||
@@ -483,6 +547,12 @@ impl ProviderSetupModalBody {
|
|||||||
self.api_key_editor.update(ctx, |editor, ctx| {
|
self.api_key_editor.update(ctx, |editor, ctx| {
|
||||||
editor.system_reset_buffer_text(self.draft_api_key.as_deref().unwrap_or_default(), ctx);
|
editor.system_reset_buffer_text(self.draft_api_key.as_deref().unwrap_or_default(), ctx);
|
||||||
});
|
});
|
||||||
|
self.project_id_editor.update(ctx, |editor, ctx| {
|
||||||
|
editor.system_reset_buffer_text(&self.draft_project_id, ctx);
|
||||||
|
});
|
||||||
|
self.location_editor.update(ctx, |editor, ctx| {
|
||||||
|
editor.system_reset_buffer_text(&self.draft_location, ctx);
|
||||||
|
});
|
||||||
self.bedrock_profile_editor.update(ctx, |editor, ctx| {
|
self.bedrock_profile_editor.update(ctx, |editor, ctx| {
|
||||||
editor.system_reset_buffer_text(&self.draft_bedrock.profile, ctx);
|
editor.system_reset_buffer_text(&self.draft_bedrock.profile, ctx);
|
||||||
});
|
});
|
||||||
@@ -513,15 +583,12 @@ impl ProviderSetupModalBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sync_provider_type_buttons(&self, ctx: &mut ViewContext<Self>) {
|
fn sync_provider_type_buttons(&self, ctx: &mut ViewContext<Self>) {
|
||||||
for (index, button) in self.provider_type_buttons.iter().enumerate() {
|
for ((button_kind, _, _), button) in PROVIDER_TYPE_OPTIONS
|
||||||
let button_kind = match index {
|
.iter()
|
||||||
0 => ProviderSetupProviderType::ChatGPTSubscription,
|
.zip(self.provider_type_buttons.iter())
|
||||||
1 => ProviderSetupProviderType::OpenAICompatible,
|
{
|
||||||
2 => ProviderSetupProviderType::Bedrock,
|
|
||||||
_ => ProviderSetupProviderType::Acp,
|
|
||||||
};
|
|
||||||
button.update(ctx, |button, ctx| {
|
button.update(ctx, |button, ctx| {
|
||||||
button.set_active(button_kind == self.provider_type, ctx);
|
button.set_active(*button_kind == self.provider_type, ctx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -555,6 +622,12 @@ impl ProviderSetupModalBody {
|
|||||||
ProviderSetupProviderType::OpenAICompatible => {
|
ProviderSetupProviderType::OpenAICompatible => {
|
||||||
self.draft_base_url.trim().is_empty()
|
self.draft_base_url.trim().is_empty()
|
||||||
}
|
}
|
||||||
|
ProviderSetupProviderType::Anthropic | ProviderSetupProviderType::Gemini => {
|
||||||
|
self.draft_api_key
|
||||||
|
.as_deref()
|
||||||
|
.is_none_or(|key| key.trim().is_empty())
|
||||||
|
}
|
||||||
|
ProviderSetupProviderType::VertexAI => self.draft_project_id.trim().is_empty(),
|
||||||
ProviderSetupProviderType::Acp => self.draft_acp.agent_id.trim().is_empty(),
|
ProviderSetupProviderType::Acp => self.draft_acp.agent_id.trim().is_empty(),
|
||||||
ProviderSetupProviderType::ChatGPTSubscription
|
ProviderSetupProviderType::ChatGPTSubscription
|
||||||
| ProviderSetupProviderType::Bedrock => false,
|
| ProviderSetupProviderType::Bedrock => false,
|
||||||
@@ -571,7 +644,10 @@ impl ProviderSetupModalBody {
|
|||||||
),
|
),
|
||||||
ProviderSetupStep::Models => match self.provider_type {
|
ProviderSetupStep::Models => match self.provider_type {
|
||||||
ProviderSetupProviderType::OpenAICompatible
|
ProviderSetupProviderType::OpenAICompatible
|
||||||
| ProviderSetupProviderType::ChatGPTSubscription => (
|
| ProviderSetupProviderType::ChatGPTSubscription
|
||||||
|
| ProviderSetupProviderType::Anthropic
|
||||||
|
| ProviderSetupProviderType::Gemini
|
||||||
|
| ProviderSetupProviderType::VertexAI => (
|
||||||
"Save",
|
"Save",
|
||||||
self.draft_name.trim().is_empty()
|
self.draft_name.trim().is_empty()
|
||||||
|| !self.draft_models.iter().any(|model| model.enabled),
|
|| !self.draft_models.iter().any(|model| model.enabled),
|
||||||
@@ -601,19 +677,48 @@ impl ProviderSetupModalBody {
|
|||||||
ProviderSetupProviderType::OpenAICompatible
|
ProviderSetupProviderType::OpenAICompatible
|
||||||
| ProviderSetupProviderType::Bedrock
|
| ProviderSetupProviderType::Bedrock
|
||||||
| ProviderSetupProviderType::Acp => OpenAIProviderKind::OpenAICompatible,
|
| ProviderSetupProviderType::Acp => OpenAIProviderKind::OpenAICompatible,
|
||||||
|
ProviderSetupProviderType::Anthropic => OpenAIProviderKind::Anthropic,
|
||||||
|
ProviderSetupProviderType::Gemini => OpenAIProviderKind::Gemini,
|
||||||
|
ProviderSetupProviderType::VertexAI => OpenAIProviderKind::VertexAI,
|
||||||
},
|
},
|
||||||
enabled: true,
|
enabled: true,
|
||||||
name: self.draft_name.trim().to_string(),
|
name: self.draft_name.trim().to_string(),
|
||||||
base_url: if self.provider_type == ProviderSetupProviderType::ChatGPTSubscription {
|
base_url: if matches!(
|
||||||
|
self.provider_type,
|
||||||
|
ProviderSetupProviderType::ChatGPTSubscription
|
||||||
|
| ProviderSetupProviderType::Anthropic
|
||||||
|
| ProviderSetupProviderType::Gemini
|
||||||
|
| ProviderSetupProviderType::VertexAI
|
||||||
|
) {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
self.draft_base_url.trim().trim_end_matches('/').to_string()
|
self.draft_base_url.trim().trim_end_matches('/').to_string()
|
||||||
},
|
},
|
||||||
api_key: self
|
api_key: matches!(
|
||||||
.draft_api_key
|
self.provider_type,
|
||||||
.as_deref()
|
ProviderSetupProviderType::OpenAICompatible
|
||||||
.filter(|key| !key.trim().is_empty())
|
| ProviderSetupProviderType::Anthropic
|
||||||
.map(str::to_string),
|
| ProviderSetupProviderType::Gemini
|
||||||
|
)
|
||||||
|
.then(|| {
|
||||||
|
self.draft_api_key
|
||||||
|
.as_deref()
|
||||||
|
.filter(|key| !key.trim().is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
})
|
||||||
|
.flatten(),
|
||||||
|
project_id: matches!(self.provider_type, ProviderSetupProviderType::VertexAI)
|
||||||
|
.then(|| self.draft_project_id.trim().to_string()),
|
||||||
|
location: matches!(self.provider_type, ProviderSetupProviderType::VertexAI).then(
|
||||||
|
|| {
|
||||||
|
let location = self.draft_location.trim();
|
||||||
|
if location.is_empty() {
|
||||||
|
"global".to_string()
|
||||||
|
} else {
|
||||||
|
location.to_string()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
models: self.draft_models.clone(),
|
models: self.draft_models.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -668,7 +773,10 @@ impl ProviderSetupModalBody {
|
|||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ProviderSetupProviderType::OpenAICompatible => {}
|
ProviderSetupProviderType::OpenAICompatible
|
||||||
|
| ProviderSetupProviderType::Anthropic
|
||||||
|
| ProviderSetupProviderType::Gemini
|
||||||
|
| ProviderSetupProviderType::VertexAI => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let provider = self.draft_provider();
|
let provider = self.draft_provider();
|
||||||
@@ -776,51 +884,53 @@ impl ProviderSetupModalBody {
|
|||||||
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
let cards = [
|
let cards = PROVIDER_TYPE_OPTIONS
|
||||||
(
|
.iter()
|
||||||
"ChatGPT subscription",
|
.enumerate()
|
||||||
"Use your ChatGPT Plus or Pro subscription with native OAuth.",
|
.map(|(index, (_, _, description))| {
|
||||||
),
|
let button = ChildView::new(&self.provider_type_buttons[index]).finish();
|
||||||
(
|
Container::new(
|
||||||
"OpenAI-compatible API",
|
Flex::column()
|
||||||
"Connect LiteLLM, Ollama, vLLM, or another compatible endpoint.",
|
.with_spacing(8.)
|
||||||
),
|
.with_child(button)
|
||||||
(
|
.with_child(
|
||||||
"AWS Bedrock",
|
Text::new(*description, appearance.ui_font_family(), INPUT_FONT_SIZE)
|
||||||
"Use the AWS Bedrock credentials and model configuration already managed by Galaxy.",
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
),
|
.soft_wrap(true)
|
||||||
(
|
.finish(),
|
||||||
"ACP agent runtime",
|
)
|
||||||
"Use a session-oriented ACP agent that owns its model and authentication.",
|
.finish(),
|
||||||
),
|
)
|
||||||
]
|
.with_padding(Padding::uniform(12.))
|
||||||
.into_iter()
|
.with_border(Border::all(1.).with_border_fill(appearance.theme().outline()))
|
||||||
.enumerate()
|
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(6.)))
|
||||||
.map(|(index, (label, description))| {
|
.finish()
|
||||||
let button = ChildView::new(&self.provider_type_buttons[index]).finish();
|
})
|
||||||
Container::new(
|
.collect::<Vec<_>>();
|
||||||
Flex::column()
|
|
||||||
.with_spacing(8.)
|
let list = Flex::column()
|
||||||
.with_child(button)
|
.with_spacing(10.)
|
||||||
.with_child(
|
.with_children(cards)
|
||||||
Text::new(description, appearance.ui_font_family(), INPUT_FONT_SIZE)
|
.finish();
|
||||||
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
let scrollable = ClippedScrollable::vertical(
|
||||||
.soft_wrap(true)
|
self.provider_type_scroll_state.clone(),
|
||||||
.finish(),
|
list,
|
||||||
)
|
ScrollbarWidth::Auto,
|
||||||
.finish(),
|
appearance.theme().nonactive_ui_detail().into(),
|
||||||
)
|
appearance.theme().active_ui_detail().into(),
|
||||||
.with_padding(Padding::uniform(12.))
|
appearance.theme().surface_1().into(),
|
||||||
.with_border(Border::all(1.).with_border_fill(appearance.theme().outline()))
|
)
|
||||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(6.)))
|
.with_overlayed_scrollbar()
|
||||||
.finish()
|
.finish();
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_spacing(16.)
|
.with_spacing(16.)
|
||||||
.with_child(description)
|
.with_child(description)
|
||||||
.with_children(cards)
|
.with_child(
|
||||||
|
ConstrainedBox::new(scrollable)
|
||||||
|
.with_max_height(360.)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -989,6 +1099,50 @@ impl ProviderSetupModalBody {
|
|||||||
.finish(),
|
.finish(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
ProviderSetupProviderType::Anthropic => {
|
||||||
|
children.push(self.render_input(appearance, "API key", &self.api_key_editor));
|
||||||
|
children.push(
|
||||||
|
Text::new(
|
||||||
|
"The key is stored locally and is never synced to the cloud. Models will be discovered from Anthropic after the connection test.",
|
||||||
|
appearance.ui_font_family(),
|
||||||
|
INPUT_FONT_SIZE,
|
||||||
|
)
|
||||||
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
|
.soft_wrap(true)
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ProviderSetupProviderType::Gemini => {
|
||||||
|
children.push(self.render_input(appearance, "API key", &self.api_key_editor));
|
||||||
|
children.push(
|
||||||
|
Text::new(
|
||||||
|
"The key is stored locally and is never synced to the cloud. Models will be discovered from Google's Gemini API after the connection test.",
|
||||||
|
appearance.ui_font_family(),
|
||||||
|
INPUT_FONT_SIZE,
|
||||||
|
)
|
||||||
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
|
.soft_wrap(true)
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ProviderSetupProviderType::VertexAI => {
|
||||||
|
children.push(self.render_input(
|
||||||
|
appearance,
|
||||||
|
"Google Cloud project ID",
|
||||||
|
&self.project_id_editor,
|
||||||
|
));
|
||||||
|
children.push(self.render_input(appearance, "Location", &self.location_editor));
|
||||||
|
children.push(
|
||||||
|
Text::new(
|
||||||
|
"Vertex AI uses Google Application Default Credentials. Run `gcloud auth application-default login` before testing the connection.",
|
||||||
|
appearance.ui_font_family(),
|
||||||
|
INPUT_FONT_SIZE,
|
||||||
|
)
|
||||||
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
|
.soft_wrap(true)
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
}
|
||||||
ProviderSetupProviderType::Bedrock => {
|
ProviderSetupProviderType::Bedrock => {
|
||||||
children.push(Self::render_label(appearance, "Authentication method"));
|
children.push(Self::render_label(appearance, "Authentication method"));
|
||||||
children.push(
|
children.push(
|
||||||
@@ -1422,7 +1576,10 @@ impl TypedActionView for ProviderSetupModalBody {
|
|||||||
}
|
}
|
||||||
ProviderSetupStep::Models => match self.provider_type {
|
ProviderSetupStep::Models => match self.provider_type {
|
||||||
ProviderSetupProviderType::OpenAICompatible
|
ProviderSetupProviderType::OpenAICompatible
|
||||||
| ProviderSetupProviderType::ChatGPTSubscription => {
|
| ProviderSetupProviderType::ChatGPTSubscription
|
||||||
|
| ProviderSetupProviderType::Anthropic
|
||||||
|
| ProviderSetupProviderType::Gemini
|
||||||
|
| ProviderSetupProviderType::VertexAI => {
|
||||||
if self.draft_name.trim().is_empty()
|
if self.draft_name.trim().is_empty()
|
||||||
|| !self.draft_models.iter().any(|model| model.enabled)
|
|| !self.draft_models.iter().any(|model| model.enabled)
|
||||||
{
|
{
|
||||||
@@ -1529,6 +1686,9 @@ fn provider_type_label(kind: ProviderSetupProviderType) -> &'static str {
|
|||||||
match kind {
|
match kind {
|
||||||
ProviderSetupProviderType::OpenAICompatible => "OpenAI-compatible API",
|
ProviderSetupProviderType::OpenAICompatible => "OpenAI-compatible API",
|
||||||
ProviderSetupProviderType::ChatGPTSubscription => "ChatGPT subscription",
|
ProviderSetupProviderType::ChatGPTSubscription => "ChatGPT subscription",
|
||||||
|
ProviderSetupProviderType::Anthropic => "Anthropic",
|
||||||
|
ProviderSetupProviderType::Gemini => "Google Gemini",
|
||||||
|
ProviderSetupProviderType::VertexAI => "Google Vertex AI",
|
||||||
ProviderSetupProviderType::Bedrock => "AWS Bedrock",
|
ProviderSetupProviderType::Bedrock => "AWS Bedrock",
|
||||||
ProviderSetupProviderType::Acp => "ACP agent runtime",
|
ProviderSetupProviderType::Acp => "ACP agent runtime",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
use galaxy_core::features::FeatureFlag;
|
use galaxy_core::features::FeatureFlag;
|
||||||
use galaxy_core::report_if_error;
|
use galaxy_core::report_if_error;
|
||||||
use galaxy_core::settings::ToggleableSetting as _;
|
use galaxy_core::settings::ToggleableSetting as _;
|
||||||
use galaxyui::fonts::Weight;
|
|
||||||
use galaxyui::keymap::ContextPredicate;
|
use galaxyui::keymap::ContextPredicate;
|
||||||
use galaxyui::ui_components::button::ButtonVariant;
|
use galaxyui::ui_components::components::UiComponent;
|
||||||
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
|
||||||
use galaxyui::ui_components::switch::SwitchStateHandle;
|
use galaxyui::ui_components::switch::SwitchStateHandle;
|
||||||
use galaxyui::{
|
use galaxyui::{
|
||||||
id, Action, AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
|
id, Action, AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use warpui::elements::{
|
use warpui::elements::{Element, MouseStateHandle};
|
||||||
Container, Element, Flex, MouseStateHandle, ParentElement, Shrinkable, Text,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::settings_page::{
|
use super::settings_page::{
|
||||||
render_body_item, AdditionalInfo, MatchData, PageType, SettingsPageMeta,
|
render_body_item, AdditionalInfo, MatchData, PageType, SettingsPageMeta,
|
||||||
@@ -22,13 +18,11 @@ use super::{
|
|||||||
SettingsAction, SettingsSection, ToggleSettingActionPair, ToggleState,
|
SettingsAction, SettingsSection, ToggleSettingActionPair, ToggleState,
|
||||||
};
|
};
|
||||||
use crate::appearance::Appearance;
|
use crate::appearance::Appearance;
|
||||||
use crate::auth::AuthStateProvider;
|
|
||||||
use crate::drive::settings::WarpDriveSettings;
|
use crate::drive::settings::WarpDriveSettings;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum WarpDriveSettingsPageAction {
|
pub enum WarpDriveSettingsPageAction {
|
||||||
ToggleShowWarpDrive,
|
ToggleShowWarpDrive,
|
||||||
SignUp,
|
|
||||||
OpenUrl(String),
|
OpenUrl(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +38,8 @@ pub fn init_actions_from_parent_view<T: Action + Clone>(
|
|||||||
WarpDriveSettingsPageAction::ToggleShowWarpDrive,
|
WarpDriveSettingsPageAction::ToggleShowWarpDrive,
|
||||||
)),
|
)),
|
||||||
SettingActionPairContexts::new(
|
SettingActionPairContexts::new(
|
||||||
context.clone() & !id!(flags::ENABLE_WARP_DRIVE) & !id!("IsAnonymousUser"),
|
context.clone() & !id!(flags::ENABLE_WARP_DRIVE),
|
||||||
context.clone() & id!(flags::ENABLE_WARP_DRIVE) & !id!("IsAnonymousUser"),
|
context.clone() & id!(flags::ENABLE_WARP_DRIVE),
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -54,10 +48,6 @@ pub fn init_actions_from_parent_view<T: Action + Clone>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum WarpDriveSettingsPageEvent {
|
|
||||||
SignUp,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WarpDriveSettingsPageView {
|
pub struct WarpDriveSettingsPageView {
|
||||||
page: PageType<Self>,
|
page: PageType<Self>,
|
||||||
}
|
}
|
||||||
@@ -66,10 +56,7 @@ impl WarpDriveSettingsPageView {
|
|||||||
pub fn new(_ctx: &mut ViewContext<Self>) -> Self {
|
pub fn new(_ctx: &mut ViewContext<Self>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
page: PageType::new_uncategorized(
|
page: PageType::new_uncategorized(
|
||||||
vec![
|
vec![Box::new(WarpDriveToggleWidget::default())],
|
||||||
Box::new(WarpDriveHeaderWidget::default()),
|
|
||||||
Box::new(WarpDriveToggleWidget::default()),
|
|
||||||
],
|
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -77,7 +64,7 @@ impl WarpDriveSettingsPageView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for WarpDriveSettingsPageView {
|
impl Entity for WarpDriveSettingsPageView {
|
||||||
type Event = WarpDriveSettingsPageEvent;
|
type Event = ();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TypedActionView for WarpDriveSettingsPageView {
|
impl TypedActionView for WarpDriveSettingsPageView {
|
||||||
@@ -91,9 +78,6 @@ impl TypedActionView for WarpDriveSettingsPageView {
|
|||||||
});
|
});
|
||||||
ctx.notify();
|
ctx.notify();
|
||||||
}
|
}
|
||||||
WarpDriveSettingsPageAction::SignUp => {
|
|
||||||
ctx.emit(WarpDriveSettingsPageEvent::SignUp);
|
|
||||||
}
|
|
||||||
WarpDriveSettingsPageAction::OpenUrl(url) => {
|
WarpDriveSettingsPageAction::OpenUrl(url) => {
|
||||||
ctx.open_url(url.as_str());
|
ctx.open_url(url.as_str());
|
||||||
}
|
}
|
||||||
@@ -139,88 +123,6 @@ impl From<ViewHandle<WarpDriveSettingsPageView>> for SettingsPageViewHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct WarpDriveHeaderWidget {
|
|
||||||
sign_up_button: MouseStateHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SettingsWidget for WarpDriveHeaderWidget {
|
|
||||||
type View = WarpDriveSettingsPageView;
|
|
||||||
|
|
||||||
fn search_terms(&self) -> &str {
|
|
||||||
"warp drive sign up"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_render(&self, app: &AppContext) -> bool {
|
|
||||||
FeatureFlag::SkipFirebaseAnonymousUser.is_enabled()
|
|
||||||
&& AuthStateProvider::as_ref(app)
|
|
||||||
.get()
|
|
||||||
.is_anonymous_or_logged_out()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(
|
|
||||||
&self,
|
|
||||||
_view: &Self::View,
|
|
||||||
appearance: &Appearance,
|
|
||||||
_app: &AppContext,
|
|
||||||
) -> Box<dyn Element> {
|
|
||||||
let ui_builder = appearance.ui_builder();
|
|
||||||
|
|
||||||
let message = Container::new(
|
|
||||||
Text::new_inline(
|
|
||||||
"To use Galaxy Drive, please create an account.".to_string(),
|
|
||||||
appearance.ui_font_family(),
|
|
||||||
14.,
|
|
||||||
)
|
|
||||||
.with_color(
|
|
||||||
appearance
|
|
||||||
.theme()
|
|
||||||
.sub_text_color(appearance.theme().surface_2())
|
|
||||||
.into_solid(),
|
|
||||||
)
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.with_margin_right(16.)
|
|
||||||
.finish();
|
|
||||||
|
|
||||||
let button = Container::new(
|
|
||||||
ui_builder
|
|
||||||
.button(ButtonVariant::Accent, self.sign_up_button.clone())
|
|
||||||
.with_style(UiComponentStyles {
|
|
||||||
font_size: Some(14.),
|
|
||||||
font_weight: Some(Weight::Semibold),
|
|
||||||
border_radius: Some(galaxyui::elements::CornerRadius::with_all(
|
|
||||||
galaxyui::elements::Radius::Pixels(4.),
|
|
||||||
)),
|
|
||||||
padding: Some(Coords {
|
|
||||||
top: 8.,
|
|
||||||
bottom: 8.,
|
|
||||||
left: 24.,
|
|
||||||
right: 24.,
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.with_text_label("Sign up".to_owned())
|
|
||||||
.build()
|
|
||||||
.on_click(move |ctx, _, _| {
|
|
||||||
ctx.dispatch_typed_action(WarpDriveSettingsPageAction::SignUp);
|
|
||||||
})
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.finish();
|
|
||||||
|
|
||||||
Container::new(
|
|
||||||
Flex::row()
|
|
||||||
.with_cross_axis_alignment(galaxyui::elements::CrossAxisAlignment::Center)
|
|
||||||
.with_child(Shrinkable::new(1., message).finish())
|
|
||||||
.with_child(button)
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.with_padding_bottom(15.)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct WarpDriveToggleWidget {
|
struct WarpDriveToggleWidget {
|
||||||
switch_state: SwitchStateHandle,
|
switch_state: SwitchStateHandle,
|
||||||
@@ -241,10 +143,6 @@ impl SettingsWidget for WarpDriveToggleWidget {
|
|||||||
app: &AppContext,
|
app: &AppContext,
|
||||||
) -> Box<dyn Element> {
|
) -> Box<dyn Element> {
|
||||||
let settings = WarpDriveSettings::as_ref(app);
|
let settings = WarpDriveSettings::as_ref(app);
|
||||||
let is_anonymous_or_logged_out = FeatureFlag::SkipFirebaseAnonymousUser.is_enabled()
|
|
||||||
&& AuthStateProvider::as_ref(app)
|
|
||||||
.get()
|
|
||||||
.is_anonymous_or_logged_out();
|
|
||||||
|
|
||||||
render_body_item::<WarpDriveSettingsPageAction>(
|
render_body_item::<WarpDriveSettingsPageAction>(
|
||||||
"Galaxy Drive".into(),
|
"Galaxy Drive".into(),
|
||||||
@@ -257,24 +155,15 @@ impl SettingsWidget for WarpDriveToggleWidget {
|
|||||||
tooltip_override_text: None,
|
tooltip_override_text: None,
|
||||||
}),
|
}),
|
||||||
LocalOnlyIconState::Hidden,
|
LocalOnlyIconState::Hidden,
|
||||||
if is_anonymous_or_logged_out {
|
ToggleState::Enabled,
|
||||||
ToggleState::Disabled
|
|
||||||
} else {
|
|
||||||
ToggleState::Enabled
|
|
||||||
},
|
|
||||||
appearance,
|
appearance,
|
||||||
appearance
|
appearance
|
||||||
.ui_builder()
|
.ui_builder()
|
||||||
.switch(self.switch_state.clone())
|
.switch(self.switch_state.clone())
|
||||||
.check(*settings.enable_warp_drive && !is_anonymous_or_logged_out)
|
.check(*settings.enable_warp_drive)
|
||||||
.with_disabled(is_anonymous_or_logged_out)
|
|
||||||
.build()
|
.build()
|
||||||
.on_click(move |ctx, _, _| {
|
.on_click(move |ctx, _, _| {
|
||||||
if !is_anonymous_or_logged_out {
|
ctx.dispatch_typed_action(WarpDriveSettingsPageAction::ToggleShowWarpDrive);
|
||||||
ctx.dispatch_typed_action(
|
|
||||||
WarpDriveSettingsPageAction::ToggleShowWarpDrive,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.finish(),
|
.finish(),
|
||||||
Some("Galaxy Drive is a workspace in your terminal where you can save Workflows, Notebooks, Prompts, and Environment Variables for personal use or to share with a team.".into()),
|
Some("Galaxy Drive is a workspace in your terminal where you can save Workflows, Notebooks, Prompts, and Environment Variables for personal use or to share with a team.".into()),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ futures.workspace = true
|
|||||||
galaxy_agent_core.workspace = true
|
galaxy_agent_core.workspace = true
|
||||||
rig-core.workspace = true
|
rig-core.workspace = true
|
||||||
rig-bedrock.workspace = true
|
rig-bedrock.workspace = true
|
||||||
|
rig-vertexai.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use galaxy_agent_core::{
|
|||||||
StopReason, ToolDefinition, TurnCommand, TurnRequest, Usage,
|
StopReason, ToolDefinition, TurnCommand, TurnRequest, Usage,
|
||||||
};
|
};
|
||||||
use rig_bedrock::streaming::{BedrockStreamingResponse, BedrockUsage};
|
use rig_bedrock::streaming::{BedrockStreamingResponse, BedrockUsage};
|
||||||
use rig_core::completion::{AssistantContent, CompletionError, GetTokenUsage, Message};
|
use rig_core::completion::{AssistantContent, CompletionError, Message};
|
||||||
use rig_core::message::{DocumentSourceKind, ToolResultContent, UserContent};
|
use rig_core::message::{DocumentSourceKind, ToolResultContent, UserContent};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -78,9 +78,10 @@ fn normalizes_bedrock_usage_and_max_token_stop() {
|
|||||||
cache_read_input_tokens: Some(40),
|
cache_read_input_tokens: Some(40),
|
||||||
cache_write_input_tokens: Some(10),
|
cache_write_input_tokens: Some(10),
|
||||||
}),
|
}),
|
||||||
|
stop_reason: None,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
map_usage(response.token_usage()),
|
map_usage((&response).into()),
|
||||||
Usage {
|
Usage {
|
||||||
input_tokens: 100,
|
input_tokens: 100,
|
||||||
output_tokens: 25,
|
output_tokens: 25,
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
mod bedrock;
|
mod bedrock;
|
||||||
mod chatgpt;
|
mod chatgpt;
|
||||||
|
mod native;
|
||||||
mod openai_compatible;
|
mod openai_compatible;
|
||||||
mod request;
|
mod request;
|
||||||
mod stream;
|
mod stream;
|
||||||
|
|
||||||
pub use bedrock::*;
|
pub use bedrock::*;
|
||||||
pub use chatgpt::*;
|
pub use chatgpt::*;
|
||||||
|
pub use native::*;
|
||||||
pub use openai_compatible::*;
|
pub use openai_compatible::*;
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use galaxy_agent_core::{
|
||||||
|
AgentError, AgentErrorKind, AgentEventStream, AgentRuntime, RuntimeCapabilities,
|
||||||
|
RuntimeDescriptor, RuntimeKind, TurnControl, TurnRequest,
|
||||||
|
};
|
||||||
|
use rig_core::client::{CompletionClient, ModelListingClient};
|
||||||
|
use rig_core::providers::{anthropic, gemini};
|
||||||
|
|
||||||
|
use crate::request::build_completion_request;
|
||||||
|
use crate::stream::{start_model_completion, start_model_turn};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct RigModelInfo {
|
||||||
|
pub id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub context_size: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn discover_anthropic_models(api_key: &str) -> Result<Vec<RigModelInfo>, String> {
|
||||||
|
let client = anthropic::Client::new(api_key).map_err(|error| error.to_string())?;
|
||||||
|
let models = client
|
||||||
|
.list_models()
|
||||||
|
.await
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
Ok(models
|
||||||
|
.into_iter()
|
||||||
|
.map(|model| RigModelInfo {
|
||||||
|
display_name: model.display_name().to_string(),
|
||||||
|
id: model.id,
|
||||||
|
context_size: model.context_length,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn discover_gemini_models(api_key: &str) -> Result<Vec<RigModelInfo>, String> {
|
||||||
|
let client = gemini::Client::new(api_key).map_err(|error| error.to_string())?;
|
||||||
|
let models = client
|
||||||
|
.list_models()
|
||||||
|
.await
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
Ok(models
|
||||||
|
.into_iter()
|
||||||
|
.filter(|model| !model.id.contains("embedding"))
|
||||||
|
.map(|model| RigModelInfo {
|
||||||
|
display_name: model.display_name().to_string(),
|
||||||
|
id: model.id,
|
||||||
|
context_size: model.context_length,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vertex_ai_model_catalog() -> Vec<RigModelInfo> {
|
||||||
|
[
|
||||||
|
(rig_vertexai::completion::GEMINI_2_5_PRO, "Gemini 2.5 Pro"),
|
||||||
|
(
|
||||||
|
rig_vertexai::completion::GEMINI_2_5_FLASH,
|
||||||
|
"Gemini 2.5 Flash",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
rig_vertexai::completion::GEMINI_2_5_FLASH_LITE,
|
||||||
|
"Gemini 2.5 Flash Lite",
|
||||||
|
),
|
||||||
|
(rig_vertexai::completion::GEMINI_1_5_PRO, "Gemini 1.5 Pro"),
|
||||||
|
(
|
||||||
|
rig_vertexai::completion::GEMINI_1_5_FLASH,
|
||||||
|
"Gemini 1.5 Flash",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, display_name)| RigModelInfo {
|
||||||
|
id: id.to_string(),
|
||||||
|
display_name: display_name.to_string(),
|
||||||
|
context_size: None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_vertex_ai_credentials(project_id: &str, location: &str) -> Result<(), String> {
|
||||||
|
rig_vertexai::Client::builder()
|
||||||
|
.with_project(project_id)
|
||||||
|
.with_location(location)
|
||||||
|
.build()
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AnthropicRuntimeConfig {
|
||||||
|
pub api_key: String,
|
||||||
|
pub model: String,
|
||||||
|
pub max_output_tokens: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AnthropicRuntime {
|
||||||
|
config: AnthropicRuntimeConfig,
|
||||||
|
descriptor: RuntimeDescriptor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnthropicRuntime {
|
||||||
|
pub fn new(config: AnthropicRuntimeConfig) -> Self {
|
||||||
|
let descriptor = native_descriptor("anthropic", &config.model);
|
||||||
|
Self { config, descriptor }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AgentRuntime for AnthropicRuntime {
|
||||||
|
fn descriptor(&self) -> &RuntimeDescriptor {
|
||||||
|
&self.descriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_turn(
|
||||||
|
&self,
|
||||||
|
request: TurnRequest,
|
||||||
|
control: TurnControl,
|
||||||
|
) -> Result<AgentEventStream, AgentError> {
|
||||||
|
if self.config.api_key.trim().is_empty() {
|
||||||
|
return Err(AgentError::new(
|
||||||
|
AgentErrorKind::Configuration,
|
||||||
|
"An Anthropic API key is required",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let client = anthropic::Client::new(&self.config.api_key)
|
||||||
|
.map_err(|error| AgentError::new(AgentErrorKind::Configuration, error.to_string()))?;
|
||||||
|
let model = client.completion_model(&self.config.model);
|
||||||
|
let max_output_tokens = request.max_output_tokens.or(self.config.max_output_tokens);
|
||||||
|
let completion_request =
|
||||||
|
build_completion_request(request, max_output_tokens, true, true, None)?;
|
||||||
|
start_model_turn(model, completion_request, control, max_output_tokens).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GeminiRuntimeConfig {
|
||||||
|
pub api_key: String,
|
||||||
|
pub model: String,
|
||||||
|
pub max_output_tokens: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GeminiRuntime {
|
||||||
|
config: GeminiRuntimeConfig,
|
||||||
|
descriptor: RuntimeDescriptor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GeminiRuntime {
|
||||||
|
pub fn new(config: GeminiRuntimeConfig) -> Self {
|
||||||
|
let descriptor = native_descriptor("gemini", &config.model);
|
||||||
|
Self { config, descriptor }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AgentRuntime for GeminiRuntime {
|
||||||
|
fn descriptor(&self) -> &RuntimeDescriptor {
|
||||||
|
&self.descriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_turn(
|
||||||
|
&self,
|
||||||
|
request: TurnRequest,
|
||||||
|
control: TurnControl,
|
||||||
|
) -> Result<AgentEventStream, AgentError> {
|
||||||
|
if self.config.api_key.trim().is_empty() {
|
||||||
|
return Err(AgentError::new(
|
||||||
|
AgentErrorKind::Configuration,
|
||||||
|
"A Gemini API key is required",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let client = gemini::Client::new(&self.config.api_key)
|
||||||
|
.map_err(|error| AgentError::new(AgentErrorKind::Configuration, error.to_string()))?;
|
||||||
|
let model = client.completion_model(&self.config.model);
|
||||||
|
let max_output_tokens = request.max_output_tokens.or(self.config.max_output_tokens);
|
||||||
|
let completion_request =
|
||||||
|
build_completion_request(request, max_output_tokens, true, true, None)?;
|
||||||
|
start_model_turn(model, completion_request, control, max_output_tokens).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct VertexAiRuntimeConfig {
|
||||||
|
pub project_id: String,
|
||||||
|
pub location: String,
|
||||||
|
pub model: String,
|
||||||
|
pub max_output_tokens: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct VertexAiRuntime {
|
||||||
|
config: VertexAiRuntimeConfig,
|
||||||
|
descriptor: RuntimeDescriptor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VertexAiRuntime {
|
||||||
|
pub fn new(config: VertexAiRuntimeConfig) -> Self {
|
||||||
|
let descriptor = native_descriptor("vertex-ai", &config.model);
|
||||||
|
Self { config, descriptor }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AgentRuntime for VertexAiRuntime {
|
||||||
|
fn descriptor(&self) -> &RuntimeDescriptor {
|
||||||
|
&self.descriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_turn(
|
||||||
|
&self,
|
||||||
|
request: TurnRequest,
|
||||||
|
control: TurnControl,
|
||||||
|
) -> Result<AgentEventStream, AgentError> {
|
||||||
|
if self.config.project_id.trim().is_empty() {
|
||||||
|
return Err(AgentError::new(
|
||||||
|
AgentErrorKind::Configuration,
|
||||||
|
"A Google Cloud project ID is required for Vertex AI",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let client = rig_vertexai::Client::builder()
|
||||||
|
.with_project(&self.config.project_id)
|
||||||
|
.with_location(if self.config.location.trim().is_empty() {
|
||||||
|
"global"
|
||||||
|
} else {
|
||||||
|
&self.config.location
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.map_err(|error| AgentError::new(AgentErrorKind::Configuration, error.to_string()))?;
|
||||||
|
let model = client.completion_model(&self.config.model);
|
||||||
|
let max_output_tokens = request.max_output_tokens.or(self.config.max_output_tokens);
|
||||||
|
let completion_request =
|
||||||
|
build_completion_request(request, max_output_tokens, true, true, None)?;
|
||||||
|
start_model_completion(model, completion_request, control, max_output_tokens).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn native_descriptor(provider: &str, model: &str) -> RuntimeDescriptor {
|
||||||
|
RuntimeDescriptor {
|
||||||
|
id: format!("rig-{provider}:{model}"),
|
||||||
|
display_name: format!("Rig / {provider} / {model}"),
|
||||||
|
kind: RuntimeKind::Provider,
|
||||||
|
capabilities: RuntimeCapabilities::provider(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::native_descriptor;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_descriptors_are_provider_specific() {
|
||||||
|
let descriptor = native_descriptor("anthropic", "claude-sonnet");
|
||||||
|
|
||||||
|
assert_eq!(descriptor.id, "rig-anthropic:claude-sonnet");
|
||||||
|
assert_eq!(descriptor.display_name, "Rig / anthropic / claude-sonnet");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,7 +76,6 @@ async fn start_model_turn<M>(
|
|||||||
) -> Result<AgentEventStream, AgentError>
|
) -> Result<AgentEventStream, AgentError>
|
||||||
where
|
where
|
||||||
M: CompletionModel + Send + Sync + 'static,
|
M: CompletionModel + Send + Sync + 'static,
|
||||||
M::StreamingResponse: Send + Sync + 'static,
|
|
||||||
{
|
{
|
||||||
let max_output_tokens = request.max_output_tokens.or(configured_max_output_tokens);
|
let max_output_tokens = request.max_output_tokens.or(configured_max_output_tokens);
|
||||||
let completion_request = build_completion_request(
|
let completion_request = build_completion_request(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use galaxy_agent_core::{
|
|||||||
AgentError, AgentErrorKind, AgentEvent, AgentEventStream, StopReason, ToolCall, TurnCommand,
|
AgentError, AgentErrorKind, AgentEvent, AgentEventStream, StopReason, ToolCall, TurnCommand,
|
||||||
TurnControl, Usage,
|
TurnControl, Usage,
|
||||||
};
|
};
|
||||||
use rig_core::completion::{CompletionError, CompletionModel, CompletionRequest, GetTokenUsage};
|
use rig_core::completion::{CompletionError, CompletionModel, CompletionRequest};
|
||||||
use rig_core::streaming::StreamedAssistantContent;
|
use rig_core::streaming::StreamedAssistantContent;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -15,7 +15,6 @@ pub(crate) async fn start_model_turn<M>(
|
|||||||
) -> Result<AgentEventStream, AgentError>
|
) -> Result<AgentEventStream, AgentError>
|
||||||
where
|
where
|
||||||
M: CompletionModel + Send + Sync + 'static,
|
M: CompletionModel + Send + Sync + 'static,
|
||||||
M::StreamingResponse: Send + Sync + 'static,
|
|
||||||
{
|
{
|
||||||
let runtime_request_id = Uuid::new_v4().to_string();
|
let runtime_request_id = Uuid::new_v4().to_string();
|
||||||
let stream_future = model.stream(completion_request).fuse();
|
let stream_future = model.stream(completion_request).fuse();
|
||||||
@@ -112,7 +111,7 @@ where
|
|||||||
// is the canonical event Galaxy consumes.
|
// is the canonical event Galaxy consumes.
|
||||||
}
|
}
|
||||||
Ok(StreamedAssistantContent::Final(response)) => {
|
Ok(StreamedAssistantContent::Final(response)) => {
|
||||||
let mapped_usage = map_usage(response.token_usage());
|
let mapped_usage = map_usage(response.usage);
|
||||||
last_output_tokens = mapped_usage.output_tokens;
|
last_output_tokens = mapped_usage.output_tokens;
|
||||||
yield Ok(AgentEvent::UsageUpdated {
|
yield Ok(AgentEvent::UsageUpdated {
|
||||||
usage: mapped_usage,
|
usage: mapped_usage,
|
||||||
@@ -142,6 +141,82 @@ where
|
|||||||
Ok(Box::pin(events))
|
Ok(Box::pin(events))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn start_model_completion<M>(
|
||||||
|
model: M,
|
||||||
|
completion_request: CompletionRequest,
|
||||||
|
control: TurnControl,
|
||||||
|
max_output_tokens: Option<u64>,
|
||||||
|
) -> Result<AgentEventStream, AgentError>
|
||||||
|
where
|
||||||
|
M: CompletionModel + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let runtime_request_id = Uuid::new_v4().to_string();
|
||||||
|
let completion_future = model.completion(completion_request).fuse();
|
||||||
|
let initial_control = control.clone();
|
||||||
|
let control_future = initial_control.receive().fuse();
|
||||||
|
futures::pin_mut!(completion_future, control_future);
|
||||||
|
|
||||||
|
let response = futures::select_biased! {
|
||||||
|
command = control_future => match command {
|
||||||
|
Ok(TurnCommand::Cancel) => {
|
||||||
|
return Ok(stopped_before_stream(runtime_request_id));
|
||||||
|
}
|
||||||
|
Ok(TurnCommand::Steer { .. }) | Err(_) => {
|
||||||
|
completion_future.await.map_err(map_completion_error)?
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result = completion_future => result.map_err(map_completion_error)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let events = async_stream::stream! {
|
||||||
|
yield Ok(AgentEvent::TurnStarted { runtime_request_id });
|
||||||
|
|
||||||
|
for content in response.choice {
|
||||||
|
match content {
|
||||||
|
rig_core::completion::AssistantContent::Text(text) => {
|
||||||
|
if !text.text.is_empty() {
|
||||||
|
yield Ok(AgentEvent::TextDelta { text: text.text });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rig_core::completion::AssistantContent::Reasoning(reasoning) => {
|
||||||
|
yield Ok(AgentEvent::ReasoningCompleted {
|
||||||
|
text: reasoning.display_text(),
|
||||||
|
signature: reasoning.first_signature().map(str::to_string),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
rig_core::completion::AssistantContent::ToolCall(tool_call) => {
|
||||||
|
yield Ok(AgentEvent::Tool {
|
||||||
|
event: galaxy_agent_core::ToolEvent::Proposed {
|
||||||
|
call: domain_tool_call(tool_call),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
rig_core::completion::AssistantContent::Image(_) => {
|
||||||
|
yield Err(AgentError::new(
|
||||||
|
AgentErrorKind::Protocol,
|
||||||
|
"Rig returned an unsupported image completion content block",
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let usage = map_usage(response.usage);
|
||||||
|
let reached_max_tokens = max_output_tokens
|
||||||
|
.is_some_and(|max| usage.output_tokens >= max);
|
||||||
|
yield Ok(AgentEvent::UsageUpdated { usage });
|
||||||
|
yield Ok(AgentEvent::TurnStopped {
|
||||||
|
reason: if reached_max_tokens {
|
||||||
|
StopReason::MaxTokens
|
||||||
|
} else {
|
||||||
|
StopReason::Completed
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Box::pin(events))
|
||||||
|
}
|
||||||
|
|
||||||
fn domain_tool_call(tool_call: rig_core::message::ToolCall) -> ToolCall {
|
fn domain_tool_call(tool_call: rig_core::message::ToolCall) -> ToolCall {
|
||||||
ToolCall {
|
ToolCall {
|
||||||
// OpenAI Responses uses a separate `call_id` for function-call output
|
// OpenAI Responses uses a separate `call_id` for function-call output
|
||||||
|
|||||||
@@ -123,8 +123,9 @@ The current UI can initially be kept alive with a temporary adapter from `AgentE
|
|||||||
### `galaxy_agent_rig`
|
### `galaxy_agent_rig`
|
||||||
|
|
||||||
Rig becomes the implementation for provider-backed conversations. The version evaluated for this
|
Rig becomes the implementation for provider-backed conversations. The version evaluated for this
|
||||||
plan is Rig 0.40.0. When introduced, it must be pinned exactly until its documented breaking-change
|
plan is Rig 0.41.0 from the pinned upstream revision
|
||||||
cadence settles for Galaxy.
|
`1f9547774edb4c269be991ac42eb043fd7b6e87f`. When introduced, it must be pinned exactly until its
|
||||||
|
documented breaking-change cadence settles for Galaxy.
|
||||||
|
|
||||||
Rig is a good fit for the provider side because it already defines a canonical completion request,
|
Rig is a good fit for the provider side because it already defines a canonical completion request,
|
||||||
provider implementations, streaming content/tool events, model history, typed tools, hooks, MCP via
|
provider implementations, streaming content/tool events, model history, typed tools, hooks, MCP via
|
||||||
@@ -137,6 +138,9 @@ Provider coverage for the first migration:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| LiteLLM / generic OpenAI-compatible | Rig OpenAI-compatible client | Custom base URL and key; preserve per-model endpoint routing. |
|
| LiteLLM / generic OpenAI-compatible | Rig OpenAI-compatible client | Custom base URL and key; preserve per-model endpoint routing. |
|
||||||
| Ollama / LM Studio | OpenAI-compatible or Rig provider adapter | Treat as explicit local/LAN endpoints. |
|
| Ollama / LM Studio | OpenAI-compatible or Rig provider adapter | Treat as explicit local/LAN endpoints. |
|
||||||
|
| Anthropic | Rig native Anthropic client | Discover models through Rig's native model-listing API and stream completions. |
|
||||||
|
| Google Gemini | Rig native Gemini client | Discover models through Rig's native model-listing API and stream completions. |
|
||||||
|
| Google Vertex AI | `rig-vertexai` companion crate | Use ADC, project/location configuration, and a bounded catalog because this Rig integration has no model-listing endpoint. |
|
||||||
| AWS Bedrock | `rig-bedrock` through the Rig facade | Preserve profile, static credential, SSO, region, and inference-profile behavior through a focused compatibility audit. |
|
| AWS Bedrock | `rig-bedrock` through the Rig facade | Preserve profile, static credential, SSO, region, and inference-profile behavior through a focused compatibility audit. |
|
||||||
| MCP tools | Rig `rmcp` tool server/client support | Reuse existing Galaxy MCP lifecycle where it is stronger; bridge tools at one boundary. |
|
| MCP tools | Rig `rmcp` tool server/client support | Reuse existing Galaxy MCP lifecycle where it is stronger; bridge tools at one boundary. |
|
||||||
|
|
||||||
@@ -258,7 +262,8 @@ Bedrock/OpenAI/ACP configuration.
|
|||||||
|
|
||||||
### Phase 2 — First Rig vertical slice: OpenAI-compatible streaming
|
### Phase 2 — First Rig vertical slice: OpenAI-compatible streaming
|
||||||
|
|
||||||
- [x] Pin `rig-core` 0.40.0 and implement one explicit OpenAI-compatible provider.
|
- [x] Pin Rig 0.41.0 to one upstream revision and implement the explicit OpenAI-compatible,
|
||||||
|
ChatGPT subscription, Anthropic, Gemini, and Vertex AI provider runtimes.
|
||||||
- [x] Support text, reasoning where available, cancellation, stop reason, usage, and persisted
|
- [x] Support text, reasoning where available, cancellation, stop reason, usage, and persisted
|
||||||
history.
|
history.
|
||||||
- [x] Route any model entry with `use_rig = true` through Rig while leaving unmarked models on the
|
- [x] Route any model entry with `use_rig = true` through Rig while leaving unmarked models on the
|
||||||
@@ -334,9 +339,9 @@ Galaxy's action model owns permission and execution lifecycle events; normalized
|
|||||||
Rig directly. The legacy Warp response envelope remains only around transcript/init/finished UI
|
Rig directly. The legacy Warp response envelope remains only around transcript/init/finished UI
|
||||||
rendering and non-Rig compatibility runtimes, not in Rig's executable tool path.
|
rendering and non-Rig compatibility runtimes, not in Rig's executable tool path.
|
||||||
|
|
||||||
### Phase 4 — Bedrock through Rig
|
### Phase 4 — Bedrock and native cloud providers through Rig
|
||||||
|
|
||||||
- [x] Pin `rig-bedrock` 0.40.0 and construct it from Galaxy's already-resolved AWS SDK client so
|
- [x] Pin `rig-bedrock` 0.41.0 and construct it from Galaxy's already-resolved AWS SDK client so
|
||||||
profile, SSO, static-key, region, and egress ownership stay at Galaxy's explicit boundary.
|
profile, SSO, static-key, region, and egress ownership stay at Galaxy's explicit boundary.
|
||||||
- [x] Resolve context markers, ARNs, existing inference profiles, and regional inference-profile
|
- [x] Resolve context markers, ARNs, existing inference profiles, and regional inference-profile
|
||||||
prefixes before passing a model ID to Rig.
|
prefixes before passing a model ID to Rig.
|
||||||
@@ -348,12 +353,17 @@ rendering and non-Rig compatibility runtimes, not in Rig's executable tool path.
|
|||||||
normalization without contacting AWS.
|
normalization without contacting AWS.
|
||||||
- [x] Preserve signed Bedrock reasoning blocks in Galaxy conversation history so adaptive-thinking
|
- [x] Preserve signed Bedrock reasoning blocks in Galaxy conversation history so adaptive-thinking
|
||||||
tool-call turns can be replayed without losing their signatures.
|
tool-call turns can be replayed without losing their signatures.
|
||||||
- [x] Define the Rig 0.40 parity policy: Galaxy retains structured tool-result error state locally
|
- [x] Define the Rig 0.41 parity policy: Galaxy retains structured tool-result error state locally
|
||||||
and sends an explicit `[ERROR]` result prefix because Rig core has no Bedrock status field;
|
and sends an explicit `[ERROR]` result prefix because Rig core has no Bedrock status field;
|
||||||
Rig owns system/message cache checkpoints, tool-schema caching is treated as an optimization,
|
Rig owns system/message cache checkpoints, tool-schema caching is treated as an optimization,
|
||||||
and one-hour cache-TTL requests stay on the compatibility runtime.
|
and one-hour cache-TTL requests stay on the compatibility runtime.
|
||||||
- [x] Add a model-by-model Rig switch to the unified Models page and route opted-in Bedrock models
|
- [x] Add a model-by-model Rig switch to the unified Models page and route opted-in Bedrock models
|
||||||
through the same request, event, permission, history, and UI adapter as OpenAI-compatible models.
|
through the same request, event, permission, history, and UI adapter as OpenAI-compatible models.
|
||||||
|
- [x] Add native Anthropic and Gemini providers with Rig-backed model discovery and streaming.
|
||||||
|
- [x] Add Vertex AI configuration with project/location and ADC validation, a bounded Rig-supported
|
||||||
|
Gemini catalog, and a non-streaming completion adapter for the current `rig-vertexai` integration.
|
||||||
|
- [x] Make the provider setup wizard's provider selector data-driven and independently scrollable so
|
||||||
|
adding the remaining Rig integrations does not expand the modal beyond the window.
|
||||||
- [ ] Run opt-in live semantic comparisons for system prompts, images, tools, reasoning, usage, and
|
- [ ] Run opt-in live semantic comparisons for system prompts, images, tools, reasoning, usage, and
|
||||||
context limits before selecting the Rig runtime for any configured Bedrock model.
|
context limits before selecting the Rig runtime for any configured Bedrock model.
|
||||||
- Keep a short-lived compatibility fallback for unsupported Bedrock behavior, measured by tests.
|
- Keep a short-lived compatibility fallback for unsupported Bedrock behavior, measured by tests.
|
||||||
@@ -390,6 +400,14 @@ metadata.
|
|||||||
persistence to the local service while keeping process lifecycle and credentials separate.
|
persistence to the local service while keeping process lifecycle and credentials separate.
|
||||||
- [x] Make the OSS channel expose only the local Personal scope and resolve it to the stable local
|
- [x] Make the OSS channel expose only the local Personal scope and resolve it to the stable local
|
||||||
owner; remote-capable channels retain their existing workspace/shared-space behavior.
|
owner; remote-capable channels retain their existing workspace/shared-space behavior.
|
||||||
|
- [x] Move personal Galaxy Drive folder creation, rename, trash/untrash, deletion, and open-state
|
||||||
|
persistence behind `LocalObjectRepository`, including recursive local-folder deletion and nested
|
||||||
|
collapse behavior; shared/team folders retain the existing remote path.
|
||||||
|
- [x] Remove the account/signup gate from Galaxy Drive visibility and Settings controls so local
|
||||||
|
Drive remains usable while logged out; team-only actions retain their separate restrictions.
|
||||||
|
- [x] Move the retained Drive import flow onto local persistence for personal targets, including
|
||||||
|
local folder/notebook/workflow creation and progress reporting without remote `UpdateManager`
|
||||||
|
or `SyncQueue` dependencies; shared/team imports retain their remote path.
|
||||||
- [ ] Replace remaining account/workspace ownership in kept content flows with local scopes.
|
- [ ] Replace remaining account/workspace ownership in kept content flows with local scopes.
|
||||||
- [ ] Remove auth, teams, billing, referral, cloud sync, GraphQL, RTC, sharing, and remote-control UI.
|
- [ ] Remove auth, teams, billing, referral, cloud sync, GraphQL, RTC, sharing, and remote-control UI.
|
||||||
|
|
||||||
@@ -406,6 +424,10 @@ assigned to the phase that owns the affected flow before the related work is con
|
|||||||
output refresh and scrolling, failure/timeout handling, and restore behavior. Add deterministic
|
output refresh and scrolling, failure/timeout handling, and restore behavior. Add deterministic
|
||||||
unit coverage and a hermetic integration flow for a command that remains active while the agent
|
unit coverage and a hermetic integration flow for a command that remains active while the agent
|
||||||
continues running.
|
continues running.
|
||||||
|
- [x] Stop takeover no longer starts a completion-assessment turn after the user cancels the
|
||||||
|
monitor.
|
||||||
|
- [x] Monitor teardown now clears orphaned in-memory state when completion metadata is missing.
|
||||||
|
- [x] Refresh requests ignore completed or no-longer-long-running blocks.
|
||||||
- [ ] Open-source project presentation: structure the About page around Galaxy’s local-first
|
- [ ] Open-source project presentation: structure the About page around Galaxy’s local-first
|
||||||
identity, audit the repository’s license and third-party notices, and make the root metadata,
|
identity, audit the repository’s license and third-party notices, and make the root metadata,
|
||||||
contribution guidance, and license files agree on the final permissive license (currently the
|
contribution guidance, and license files agree on the final permissive license (currently the
|
||||||
@@ -460,12 +482,10 @@ contract is what the UI and persistence observe.
|
|||||||
| Telemetry/remote logs/remote flags | Remove, not merely default-off. |
|
| Telemetry/remote logs/remote flags | Remove, not merely default-off. |
|
||||||
| SSH and remote Git | Keep as explicit user-initiated remote development boundaries. |
|
| SSH and remote Git | Keep as explicit user-initiated remote development boundaries. |
|
||||||
| Web/network agent tools | Disabled by default and permission-visible. |
|
| Web/network agent tools | Disabled by default and permission-visible. |
|
||||||
| Rig dependency | Exact version pin with upgrade contract tests. |
|
| Rig dependency | Exact upstream revision pin with upgrade contract tests. |
|
||||||
|
|
||||||
## Immediate next vertical slice
|
## Immediate next vertical slice
|
||||||
|
|
||||||
Continue Phase 6 by replacing remaining account/workspace ownership with local scopes and removing
|
Continue Phase 6 by replacing remaining account/workspace ownership with local scopes and removing
|
||||||
cloud identity UI from kept flows. The next bounded implementation is to move the remaining Galaxy
|
cloud identity UI from kept flows. Live Phase 4 Bedrock semantic comparisons remain an explicit
|
||||||
Drive folder mutations (create, rename, trash, and open-state persistence) behind
|
|
||||||
`LocalObjectRepository` for OSS. Live Phase 4 Bedrock semantic comparisons remain an explicit
|
|
||||||
opt-in validation task because they require configured AWS access.
|
opt-in validation task because they require configured AWS access.
|
||||||
|
|||||||
Reference in New Issue
Block a user