From f8b8cce972a6b5560f44d1e72a104f7d880e3f27 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sat, 25 Apr 2026 15:38:58 +0800 Subject: [PATCH 01/62] feat(inference): add provider store --- Cargo.lock | 13 + Cargo.toml | 1 + crates/inference/Cargo.toml | 26 ++ crates/inference/src/credentials.rs | 53 ++++ crates/inference/src/error.rs | 50 ++++ crates/inference/src/lib.rs | 20 ++ crates/inference/src/model.rs | 156 ++++++++++ crates/inference/src/store.rs | 438 ++++++++++++++++++++++++++++ 8 files changed, 757 insertions(+) create mode 100644 crates/inference/Cargo.toml create mode 100644 crates/inference/src/credentials.rs create mode 100644 crates/inference/src/error.rs create mode 100644 crates/inference/src/lib.rs create mode 100644 crates/inference/src/model.rs create mode 100644 crates/inference/src/store.rs diff --git a/Cargo.lock b/Cargo.lock index 168be109..60e058e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,6 +123,19 @@ dependencies = [ "url", ] +[[package]] +name = "aghub-inference" +version = "0.1.2" +dependencies = [ + "keyring", + "serde", + "serde_json", + "tauri", + "tempfile", + "thiserror 2.0.18", + "uuid", +] + [[package]] name = "aghub-markdown" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index fd5a752d..d956c798 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/skill", "crates/git", "crates/markdown", + "crates/inference", "crates/desktop/src-tauri", ] resolver = "2" diff --git a/crates/inference/Cargo.toml b/crates/inference/Cargo.toml new file mode 100644 index 00000000..3c4ec8db --- /dev/null +++ b/crates/inference/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "aghub-inference" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +description = "Inference provider configuration storage for aghub" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +keyring = { version = "3", features = [ + "apple-native", + "windows-native", + "linux-native", +] } +uuid = { version = "1", features = [ "v4" ] } +tauri = { version = "2", optional = true } + +[dev-dependencies] +tempfile = { workspace = true } + +[features] +default = [ ] +tauri = [ "dep:tauri" ] diff --git a/crates/inference/src/credentials.rs b/crates/inference/src/credentials.rs new file mode 100644 index 00000000..be0385ee --- /dev/null +++ b/crates/inference/src/credentials.rs @@ -0,0 +1,53 @@ +//! API key storage for inference providers. + +use crate::error::Result; + +const KEYRING_SERVICE: &str = "aghub.inference_provider"; + +/// Stores provider API keys outside of `inference_providers.json`. +pub trait CredentialStore { + /// Read a provider API key. + fn get_api_key(&self, provider_id: &str) -> Result>; + + /// Store a provider API key. + fn set_api_key(&self, provider_id: &str, api_key: &str) -> Result<()>; + + /// Delete a provider API key. + fn delete_api_key(&self, provider_id: &str) -> Result<()>; +} + +/// Platform-native keyring implementation. +#[derive(Debug, Clone, Copy, Default)] +pub struct NativeCredentialStore; + +impl NativeCredentialStore { + fn entry(provider_id: &str) -> Result { + let user = format!("provider:{provider_id}:api_key"); + Ok(keyring::Entry::new(KEYRING_SERVICE, &user)?) + } +} + +impl CredentialStore for NativeCredentialStore { + fn get_api_key(&self, provider_id: &str) -> Result> { + let entry = Self::entry(provider_id)?; + match entry.get_password() { + Ok(api_key) => Ok(Some(api_key)), + Err(keyring::Error::NoEntry) => Ok(None), + Err(error) => Err(error.into()), + } + } + + fn set_api_key(&self, provider_id: &str, api_key: &str) -> Result<()> { + let entry = Self::entry(provider_id)?; + entry.set_password(api_key)?; + Ok(()) + } + + fn delete_api_key(&self, provider_id: &str) -> Result<()> { + let entry = Self::entry(provider_id)?; + match entry.delete_credential() { + Ok(()) | Err(keyring::Error::NoEntry) => Ok(()), + Err(error) => Err(error.into()), + } + } +} diff --git a/crates/inference/src/error.rs b/crates/inference/src/error.rs new file mode 100644 index 00000000..020cf58b --- /dev/null +++ b/crates/inference/src/error.rs @@ -0,0 +1,50 @@ +//! Error types for inference provider storage. + +/// Error type for inference provider operations. +#[derive(Debug, thiserror::Error)] +pub enum InferenceProviderError { + /// I/O error. + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + /// JSON serialization or deserialization error. + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + + /// Platform keyring error. + #[error("keyring error: {0}")] + Keyring(String), + + /// Provider names must not be empty. + #[error("provider name cannot be empty")] + EmptyName, + + /// API keys must not be empty. + #[error("provider API key cannot be empty")] + EmptyApiKey, + + /// Provider name is already in use. + #[error("provider already exists: {0}")] + AlreadyExists(String), + + /// Provider format is not supported. + #[error("unsupported inference provider format: {0}")] + InvalidFormat(String), + + /// Provider ID was not found. + #[error("provider not found: {0}")] + NotFound(String), + + /// Tauri app data directory could not be resolved. + #[error("failed to resolve Tauri app data directory: {0}")] + AppDataDir(String), +} + +impl From for InferenceProviderError { + fn from(error: keyring::Error) -> Self { + Self::Keyring(error.to_string()) + } +} + +/// Result type alias for inference provider operations. +pub type Result = std::result::Result; diff --git a/crates/inference/src/lib.rs b/crates/inference/src/lib.rs new file mode 100644 index 00000000..a7012eef --- /dev/null +++ b/crates/inference/src/lib.rs @@ -0,0 +1,20 @@ +//! Inference provider configuration storage. +//! +//! Provider metadata is stored in `inference_providers.json` under the app data +//! directory. API keys are stored separately via the platform-native keyring. + +pub mod credentials; +pub mod error; +pub mod model; +pub mod store; + +pub use credentials::{CredentialStore, NativeCredentialStore}; +pub use error::{InferenceProviderError, Result}; +pub use model::{ + CreateInferenceProvider, InferenceProvider, InferenceProviderFormat, + UpdateInferenceProvider, +}; +pub use store::{ + InferenceProviderRepository, InferenceProviderStore, + INFERENCE_PROVIDERS_FILE, +}; diff --git a/crates/inference/src/model.rs b/crates/inference/src/model.rs new file mode 100644 index 00000000..c5fb2117 --- /dev/null +++ b/crates/inference/src/model.rs @@ -0,0 +1,156 @@ +//! Inference provider data models. + +use std::{fmt, str::FromStr}; + +use serde::{Deserialize, Serialize}; + +use crate::error::InferenceProviderError; + +/// Wire format used by an inference provider. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum InferenceProviderFormat { + /// Anthropic Messages API compatible format. + #[serde(rename = "anthropic")] + Anthropic, + + /// OpenAI Chat Completions compatible format. + #[serde( + rename = "openai_completions", + alias = "openai_completion", + alias = "openai-chat-completions", + alias = "openai_chat_completions" + )] + OpenAiCompletions, + + /// OpenAI Responses API compatible format. + #[serde( + rename = "openai_responses", + alias = "openai_response", + alias = "openai-responses" + )] + OpenAiResponses, +} + +impl fmt::Display for InferenceProviderFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let value = match self { + Self::Anthropic => "anthropic", + Self::OpenAiCompletions => "openai_completions", + Self::OpenAiResponses => "openai_responses", + }; + f.write_str(value) + } +} + +impl FromStr for InferenceProviderFormat { + type Err = InferenceProviderError; + + fn from_str(value: &str) -> Result { + match value.trim().to_ascii_lowercase().as_str() { + "anthropic" => Ok(Self::Anthropic), + "openai_completion" + | "openai_completions" + | "openai-chat-completions" + | "openai_chat_completions" => Ok(Self::OpenAiCompletions), + "openai_response" | "openai_responses" | "openai-responses" => { + Ok(Self::OpenAiResponses) + } + other => { + Err(InferenceProviderError::InvalidFormat(other.to_string())) + } + } + } +} + +/// Persisted inference provider metadata. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InferenceProvider { + /// Stable provider identifier. + pub id: String, + + /// User-visible provider name. + pub name: String, + + /// Request/response format supported by this provider. + pub format: InferenceProviderFormat, +} + +/// Provider creation input. +#[derive(Clone, Deserialize)] +pub struct CreateInferenceProvider { + /// User-visible provider name. + pub name: String, + + /// Request/response format supported by this provider. + pub format: InferenceProviderFormat, + + /// Provider API key. This is write-only and never stored in JSON. + pub api_key: String, +} + +impl fmt::Debug for CreateInferenceProvider { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CreateInferenceProvider") + .field("name", &self.name) + .field("format", &self.format) + .field("api_key", &"[redacted]") + .finish() + } +} + +/// Provider update input. +#[derive(Clone, Default, Deserialize)] +pub struct UpdateInferenceProvider { + /// Updated user-visible provider name. + pub name: Option, + + /// Updated request/response format. + pub format: Option, + + /// Updated provider API key. This is write-only and never stored in JSON. + pub api_key: Option, +} + +impl fmt::Debug for UpdateInferenceProvider { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UpdateInferenceProvider") + .field("name", &self.name) + .field("format", &self.format) + .field("api_key", &self.api_key.as_ref().map(|_| "[redacted]")) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_serialization() { + assert_eq!( + serde_json::to_string(&InferenceProviderFormat::Anthropic).unwrap(), + r#""anthropic""# + ); + assert_eq!( + serde_json::to_string(&InferenceProviderFormat::OpenAiCompletions) + .unwrap(), + r#""openai_completions""# + ); + assert_eq!( + serde_json::to_string(&InferenceProviderFormat::OpenAiResponses) + .unwrap(), + r#""openai_responses""# + ); + } + + #[test] + fn test_format_aliases() { + let completions: InferenceProviderFormat = + serde_json::from_str(r#""openai_completion""#).unwrap(); + let responses: InferenceProviderFormat = + serde_json::from_str(r#""openai_response""#).unwrap(); + + assert_eq!(completions, InferenceProviderFormat::OpenAiCompletions); + assert_eq!(responses, InferenceProviderFormat::OpenAiResponses); + } +} diff --git a/crates/inference/src/store.rs b/crates/inference/src/store.rs new file mode 100644 index 00000000..2114b665 --- /dev/null +++ b/crates/inference/src/store.rs @@ -0,0 +1,438 @@ +//! CRUD storage for inference providers. + +use std::fs; +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; + +use crate::credentials::{CredentialStore, NativeCredentialStore}; +use crate::error::{InferenceProviderError, Result}; +use crate::model::{ + CreateInferenceProvider, InferenceProvider, UpdateInferenceProvider, +}; + +/// File name under the Tauri app data directory. +pub const INFERENCE_PROVIDERS_FILE: &str = "inference_providers.json"; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +struct InferenceProvidersFile { + #[serde(default)] + providers: Vec, +} + +/// CRUD interface for inference provider metadata and API keys. +pub trait InferenceProviderRepository { + /// List all providers. + fn list(&self) -> Result>; + + /// Get one provider by ID. + fn get(&self, id: &str) -> Result; + + /// Create a provider and store its API key in the native credential store. + fn create( + &self, + input: CreateInferenceProvider, + ) -> Result; + + /// Update provider metadata and optionally replace its API key. + fn update( + &self, + id: &str, + input: UpdateInferenceProvider, + ) -> Result; + + /// Delete provider metadata and its API key. + fn delete(&self, id: &str) -> Result; + + /// Read the provider API key from the native credential store. + fn get_api_key(&self, id: &str) -> Result>; + + /// Replace the provider API key in the native credential store. + fn set_api_key(&self, id: &str, api_key: &str) -> Result<()>; + + /// Delete the provider API key from the native credential store. + fn delete_api_key(&self, id: &str) -> Result<()>; +} + +/// File-backed inference provider store. +#[derive(Debug, Clone)] +pub struct InferenceProviderStore { + app_data_dir: PathBuf, + credentials: C, +} + +impl InferenceProviderStore { + /// Create a store rooted at a Tauri app data directory path. + pub fn new(app_data_dir: impl Into) -> Self { + Self::with_credentials(app_data_dir, NativeCredentialStore) + } + + /// Create a store from the current Tauri app handle. + #[cfg(feature = "tauri")] + pub fn from_tauri( + app: &tauri::AppHandle, + ) -> Result { + use tauri::Manager; + + let app_data_dir = app.path().app_data_dir().map_err(|error| { + InferenceProviderError::AppDataDir(error.to_string()) + })?; + Ok(Self::new(app_data_dir)) + } +} + +impl InferenceProviderStore { + /// Create a store with an explicit credential store implementation. + pub fn with_credentials( + app_data_dir: impl Into, + credentials: C, + ) -> Self { + Self { + app_data_dir: app_data_dir.into(), + credentials, + } + } + + /// App data directory used by this store. + pub fn app_data_dir(&self) -> &Path { + &self.app_data_dir + } + + /// Full path to `inference_providers.json`. + pub fn file_path(&self) -> PathBuf { + self.app_data_dir.join(INFERENCE_PROVIDERS_FILE) + } +} + +impl InferenceProviderStore { + fn read_file(&self) -> Result { + let path = self.file_path(); + let contents = match fs::read_to_string(&path) { + Ok(contents) => contents, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return Ok(InferenceProvidersFile::default()); + } + Err(error) => return Err(error.into()), + }; + + if contents.trim().is_empty() { + return Ok(InferenceProvidersFile::default()); + } + + Ok(serde_json::from_str(&contents)?) + } + + fn write_file(&self, file: &InferenceProvidersFile) -> Result<()> { + let path = self.file_path(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + let json = serde_json::to_string_pretty(file)?; + fs::write(path, json)?; + Ok(()) + } + + fn find_index(providers: &[InferenceProvider], id: &str) -> Result { + providers + .iter() + .position(|provider| provider.id == id) + .ok_or_else(|| InferenceProviderError::NotFound(id.to_string())) + } + + fn ensure_unique_name( + providers: &[InferenceProvider], + name: &str, + ignore_id: Option<&str>, + ) -> Result<()> { + let exists = providers.iter().any(|provider| { + ignore_id != Some(provider.id.as_str()) + && provider.name.eq_ignore_ascii_case(name) + }); + + if exists { + Err(InferenceProviderError::AlreadyExists(name.to_string())) + } else { + Ok(()) + } + } +} + +impl InferenceProviderRepository + for InferenceProviderStore +{ + fn list(&self) -> Result> { + Ok(self.read_file()?.providers) + } + + fn get(&self, id: &str) -> Result { + let file = self.read_file()?; + let index = Self::find_index(&file.providers, id)?; + Ok(file.providers[index].clone()) + } + + fn create( + &self, + input: CreateInferenceProvider, + ) -> Result { + let name = clean_name(&input.name)?; + ensure_api_key(&input.api_key)?; + + let mut file = self.read_file()?; + Self::ensure_unique_name(&file.providers, &name, None)?; + + let provider = InferenceProvider { + id: uuid::Uuid::new_v4().to_string(), + name, + format: input.format, + }; + + self.credentials.set_api_key(&provider.id, &input.api_key)?; + file.providers.push(provider.clone()); + if let Err(error) = self.write_file(&file) { + let _ = self.credentials.delete_api_key(&provider.id); + return Err(error); + } + + Ok(provider) + } + + fn update( + &self, + id: &str, + input: UpdateInferenceProvider, + ) -> Result { + let mut file = self.read_file()?; + let index = Self::find_index(&file.providers, id)?; + + if let Some(ref name) = input.name { + let name = clean_name(name)?; + Self::ensure_unique_name(&file.providers, &name, Some(id))?; + file.providers[index].name = name; + } + + if let Some(format) = input.format { + file.providers[index].format = format; + } + + let previous_api_key = match input.api_key.as_ref() { + Some(api_key) => { + ensure_api_key(api_key)?; + let previous = self.credentials.get_api_key(id)?; + self.credentials.set_api_key(id, api_key)?; + Some(previous) + } + None => None, + }; + + if let Err(error) = self.write_file(&file) { + if let Some(previous_api_key) = previous_api_key { + match previous_api_key { + Some(api_key) => { + let _ = self.credentials.set_api_key(id, &api_key); + } + None => { + let _ = self.credentials.delete_api_key(id); + } + } + } + return Err(error); + } + + Ok(file.providers[index].clone()) + } + + fn delete(&self, id: &str) -> Result { + let mut file = self.read_file()?; + let index = Self::find_index(&file.providers, id)?; + let provider = file.providers.remove(index); + let previous_api_key = self.credentials.get_api_key(id)?; + + self.credentials.delete_api_key(id)?; + if let Err(error) = self.write_file(&file) { + if let Some(api_key) = previous_api_key { + let _ = self.credentials.set_api_key(id, &api_key); + } + return Err(error); + } + + Ok(provider) + } + + fn get_api_key(&self, id: &str) -> Result> { + self.get(id)?; + self.credentials.get_api_key(id) + } + + fn set_api_key(&self, id: &str, api_key: &str) -> Result<()> { + self.get(id)?; + ensure_api_key(api_key)?; + self.credentials.set_api_key(id, api_key) + } + + fn delete_api_key(&self, id: &str) -> Result<()> { + self.get(id)?; + self.credentials.delete_api_key(id) + } +} + +fn clean_name(name: &str) -> Result { + let name = name.trim(); + if name.is_empty() { + Err(InferenceProviderError::EmptyName) + } else { + Ok(name.to_string()) + } +} + +fn ensure_api_key(api_key: &str) -> Result<()> { + if api_key.trim().is_empty() { + Err(InferenceProviderError::EmptyApiKey) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::sync::{Arc, Mutex}; + + use super::*; + use crate::model::InferenceProviderFormat; + + #[derive(Debug, Clone, Default)] + struct MemoryCredentialStore { + values: Arc>>, + } + + impl CredentialStore for MemoryCredentialStore { + fn get_api_key(&self, provider_id: &str) -> Result> { + Ok(self.values.lock().unwrap().get(provider_id).cloned()) + } + + fn set_api_key(&self, provider_id: &str, api_key: &str) -> Result<()> { + self.values + .lock() + .unwrap() + .insert(provider_id.to_string(), api_key.to_string()); + Ok(()) + } + + fn delete_api_key(&self, provider_id: &str) -> Result<()> { + self.values.lock().unwrap().remove(provider_id); + Ok(()) + } + } + + fn store() -> ( + tempfile::TempDir, + InferenceProviderStore, + ) { + let temp = tempfile::tempdir().unwrap(); + let store = InferenceProviderStore::with_credentials( + temp.path(), + MemoryCredentialStore::default(), + ); + (temp, store) + } + + #[test] + fn test_list_missing_file_is_empty() { + let (_temp, store) = store(); + + assert!(store.list().unwrap().is_empty()); + } + + #[test] + fn test_create_stores_metadata_without_api_key() { + let (_temp, store) = store(); + + let provider = store + .create(CreateInferenceProvider { + name: "OpenAI".to_string(), + format: InferenceProviderFormat::OpenAiResponses, + api_key: "sk-test".to_string(), + }) + .unwrap(); + + let contents = fs::read_to_string(store.file_path()).unwrap(); + assert!(contents.contains("OpenAI")); + assert!(contents.contains("openai_responses")); + assert!(!contents.contains("sk-test")); + assert_eq!( + store.get_api_key(&provider.id).unwrap(), + Some("sk-test".to_string()) + ); + } + + #[test] + fn test_update_provider_metadata_and_api_key() { + let (_temp, store) = store(); + let provider = store + .create(CreateInferenceProvider { + name: "Anthropic".to_string(), + format: InferenceProviderFormat::Anthropic, + api_key: "first-key".to_string(), + }) + .unwrap(); + + let updated = store + .update( + &provider.id, + UpdateInferenceProvider { + name: Some("Claude".to_string()), + format: Some(InferenceProviderFormat::OpenAiCompletions), + api_key: Some("second-key".to_string()), + }, + ) + .unwrap(); + + assert_eq!(updated.name, "Claude"); + assert_eq!(updated.format, InferenceProviderFormat::OpenAiCompletions); + assert_eq!( + store.get_api_key(&provider.id).unwrap(), + Some("second-key".to_string()) + ); + } + + #[test] + fn test_delete_provider_and_api_key() { + let (_temp, store) = store(); + let provider = store + .create(CreateInferenceProvider { + name: "Anthropic".to_string(), + format: InferenceProviderFormat::Anthropic, + api_key: "secret".to_string(), + }) + .unwrap(); + + let deleted = store.delete(&provider.id).unwrap(); + + assert_eq!(deleted.id, provider.id); + assert!(store.list().unwrap().is_empty()); + assert_eq!(store.credentials.get_api_key(&provider.id).unwrap(), None); + } + + #[test] + fn test_duplicate_name_is_rejected() { + let (_temp, store) = store(); + store + .create(CreateInferenceProvider { + name: "OpenAI".to_string(), + format: InferenceProviderFormat::OpenAiResponses, + api_key: "first".to_string(), + }) + .unwrap(); + + let error = store + .create(CreateInferenceProvider { + name: "openai".to_string(), + format: InferenceProviderFormat::OpenAiCompletions, + api_key: "second".to_string(), + }) + .unwrap_err(); + + assert!(matches!(error, InferenceProviderError::AlreadyExists(_))); + } +} From b01694f7c26f0d74c3c813f0e1cbd31a58720fd1 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sat, 25 Apr 2026 16:17:55 +0800 Subject: [PATCH 02/62] feat(api): add inference provider endpoints --- Cargo.lock | 1 + crates/api/Cargo.toml | 1 + crates/api/src/bin/export-dto.rs | 10 ++ crates/api/src/dto/inference.rs | 110 +++++++++++++++++ crates/api/src/dto/mod.rs | 1 + crates/api/src/error.rs | 37 ++++++ crates/api/src/lib.rs | 26 ++++ crates/api/src/main.rs | 4 +- crates/api/src/routes/inference.rs | 112 ++++++++++++++++++ crates/api/src/routes/mod.rs | 1 + crates/api/src/state.rs | 5 + .../desktop/src-tauri/src/commands/server.rs | 10 +- .../dto/CreateInferenceProviderRequest.ts | 8 ++ .../dto/InferenceProviderFormatDto.ts | 6 + .../dto/InferenceProviderPasswordResponse.ts | 6 + .../dto/InferenceProviderResponse.ts | 8 ++ .../dto/UpdateInferenceProviderRequest.ts | 8 ++ crates/desktop/src/generated/dto/index.ts | 5 + crates/desktop/src/lib/api.ts | 40 +++++++ crates/desktop/src/requests/keys.ts | 6 + 20 files changed, 401 insertions(+), 4 deletions(-) create mode 100644 crates/api/src/dto/inference.rs create mode 100644 crates/api/src/routes/inference.rs create mode 100644 crates/desktop/src/generated/dto/CreateInferenceProviderRequest.ts create mode 100644 crates/desktop/src/generated/dto/InferenceProviderFormatDto.ts create mode 100644 crates/desktop/src/generated/dto/InferenceProviderPasswordResponse.ts create mode 100644 crates/desktop/src/generated/dto/InferenceProviderResponse.ts create mode 100644 crates/desktop/src/generated/dto/UpdateInferenceProviderRequest.ts diff --git a/Cargo.lock b/Cargo.lock index 60e058e5..9c210ac9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,7 @@ version = "0.1.2" dependencies = [ "aghub-core", "aghub-git", + "aghub-inference", "dirs", "keyring", "log", diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 27a24d24..506c771c 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -18,6 +18,7 @@ crate-type = [ "rlib" ] [dependencies] aghub-core = { path = "../core" } aghub-git = { path = "../git" } +aghub-inference = { path = "../inference" } skill = { path = "../skill" } skills-sh = { path = "../skills-sh" } rocket = { version = "0.5", features = [ "json" ] } diff --git a/crates/api/src/bin/export-dto.rs b/crates/api/src/bin/export-dto.rs index 78ff06c8..7b4490b5 100644 --- a/crates/api/src/bin/export-dto.rs +++ b/crates/api/src/bin/export-dto.rs @@ -12,6 +12,11 @@ use aghub_api::dto::{ }, common::ConfigSource, credential::{CreateCredentialRequest, CredentialResponse}, + inference::{ + CreateInferenceProviderRequest, InferenceProviderFormatDto, + InferenceProviderPasswordResponse, InferenceProviderResponse, + UpdateInferenceProviderRequest, + }, integrations::{ CodeEditorType, EditSkillFolderRequest, OpenSkillFolderRequest, OpenWithEditorRequest, ToolInfoDto, ToolPreferencesDto, @@ -113,6 +118,11 @@ fn main() -> Result<(), Box> { export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; + export_type::(&cfg)?; + export_type::(&cfg)?; + export_type::(&cfg)?; + export_type::(&cfg)?; + export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; diff --git a/crates/api/src/dto/inference.rs b/crates/api/src/dto/inference.rs new file mode 100644 index 00000000..f4558d27 --- /dev/null +++ b/crates/api/src/dto/inference.rs @@ -0,0 +1,110 @@ +use aghub_inference::{ + CreateInferenceProvider, InferenceProvider, InferenceProviderFormat, + UpdateInferenceProvider, +}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +#[derive(Debug, Clone, Copy, Deserialize, Serialize, TS)] +#[ts(export)] +#[serde(rename_all = "snake_case")] +pub enum InferenceProviderFormatDto { + Anthropic, + #[serde(rename = "openai_completions")] + OpenAiCompletions, + #[serde(rename = "openai_responses")] + OpenAiResponses, +} + +impl From for InferenceProviderFormatDto { + fn from(value: InferenceProviderFormat) -> Self { + match value { + InferenceProviderFormat::Anthropic => Self::Anthropic, + InferenceProviderFormat::OpenAiCompletions => { + Self::OpenAiCompletions + } + InferenceProviderFormat::OpenAiResponses => Self::OpenAiResponses, + } + } +} + +impl From for InferenceProviderFormat { + fn from(value: InferenceProviderFormatDto) -> Self { + match value { + InferenceProviderFormatDto::Anthropic => Self::Anthropic, + InferenceProviderFormatDto::OpenAiCompletions => { + Self::OpenAiCompletions + } + InferenceProviderFormatDto::OpenAiResponses => { + Self::OpenAiResponses + } + } + } +} + +#[derive(Debug, Deserialize, TS)] +#[ts(export)] +pub struct CreateInferenceProviderRequest { + pub name: String, + pub format: InferenceProviderFormatDto, + pub api_key: String, +} + +impl From for CreateInferenceProvider { + fn from(req: CreateInferenceProviderRequest) -> Self { + CreateInferenceProvider { + name: req.name, + format: req.format.into(), + api_key: req.api_key, + } + } +} + +#[derive(Debug, Deserialize, TS)] +#[ts(export)] +pub struct UpdateInferenceProviderRequest { + pub name: Option, + pub format: Option, + pub api_key: Option, +} + +impl From for UpdateInferenceProvider { + fn from(req: UpdateInferenceProviderRequest) -> Self { + UpdateInferenceProvider { + name: req.name, + format: req.format.map(Into::into), + api_key: req.api_key, + } + } +} + +#[derive(Debug, Serialize, TS)] +#[ts(export)] +pub struct InferenceProviderResponse { + pub id: String, + pub name: String, + pub format: InferenceProviderFormatDto, +} + +impl From for InferenceProviderResponse { + fn from(provider: InferenceProvider) -> Self { + InferenceProviderResponse::from(&provider) + } +} + +impl From<&InferenceProvider> for InferenceProviderResponse { + fn from(provider: &InferenceProvider) -> Self { + InferenceProviderResponse { + id: provider.id.clone(), + name: provider.name.clone(), + format: provider.format.into(), + } + } +} + +#[derive(Debug, Serialize, TS)] +#[ts(export)] +pub struct InferenceProviderPasswordResponse { + pub name: String, + pub api_key: String, +} diff --git a/crates/api/src/dto/mod.rs b/crates/api/src/dto/mod.rs index b771f11f..d3dfe93f 100644 --- a/crates/api/src/dto/mod.rs +++ b/crates/api/src/dto/mod.rs @@ -1,6 +1,7 @@ pub mod agents; pub mod common; pub mod credential; +pub mod inference; pub mod integrations; pub mod market; pub mod mcp; diff --git a/crates/api/src/error.rs b/crates/api/src/error.rs index 9a01d4ff..edddbf0c 100644 --- a/crates/api/src/error.rs +++ b/crates/api/src/error.rs @@ -1,4 +1,5 @@ use aghub_core::errors::ConfigError; +use aghub_inference::InferenceProviderError; use rocket::http::{ContentType, Status}; use rocket::response::{self, Responder}; use rocket::serde::json::serde_json; @@ -82,6 +83,42 @@ impl From for ApiError { } } +impl From for ApiError { + fn from(e: InferenceProviderError) -> Self { + match e { + InferenceProviderError::EmptyName + | InferenceProviderError::EmptyApiKey + | InferenceProviderError::InvalidFormat(_) => ApiError::new( + Status::BadRequest, + e.to_string(), + "INVALID_PARAM", + ), + InferenceProviderError::AlreadyExists(_) => ApiError::new( + Status::Conflict, + e.to_string(), + "RESOURCE_EXISTS", + ), + InferenceProviderError::NotFound(_) => ApiError::new( + Status::NotFound, + e.to_string(), + "RESOURCE_NOT_FOUND", + ), + InferenceProviderError::Keyring(_) => ApiError::new( + Status::InternalServerError, + e.to_string(), + "KEYCHAIN_ERROR", + ), + InferenceProviderError::Io(_) + | InferenceProviderError::Json(_) + | InferenceProviderError::AppDataDir(_) => ApiError::new( + Status::InternalServerError, + e.to_string(), + "INFERENCE_PROVIDER_STORE_ERROR", + ), + } + } +} + impl<'r> Responder<'r, 'static> for ApiError { fn respond_to( self, diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 7a9c062a..2fe8ce49 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -1,6 +1,8 @@ #[macro_use] extern crate rocket; +use std::path::PathBuf; + use log::{debug, error, info, warn}; use rocket::{ fairing::{Fairing, Info, Kind}, @@ -16,6 +18,22 @@ pub mod state; pub struct ApiOptions { pub port: u16, + pub app_data_dir: Option, +} + +impl ApiOptions { + pub fn new(port: u16) -> Self { + Self { + port, + app_data_dir: None, + } + } +} + +fn default_app_data_dir() -> PathBuf { + dirs::data_dir() + .unwrap_or_else(std::env::temp_dir) + .join("aghub") } struct ApiLogFairing; @@ -70,6 +88,8 @@ impl Fairing for ApiLogFairing { pub async fn start(options: ApiOptions) -> Result<(), rocket::Error> { info!("starting aghub API server on 127.0.0.1:{}", options.port); + let app_data_dir = + options.app_data_dir.unwrap_or_else(default_app_data_dir); let config = rocket::Config { port: options.port, address: std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST), @@ -103,6 +123,7 @@ pub async fn start(options: ApiOptions) -> Result<(), rocket::Error> { .manage(crate::state::GitCloneSessions { sessions: std::sync::Mutex::new(std::collections::HashMap::new()), }) + .manage(crate::state::InferenceProviderState { app_data_dir }) .mount( "/api/v1", routes![ @@ -145,6 +166,11 @@ pub async fn start(options: ApiOptions) -> Result<(), rocket::Error> { routes::credentials::list_credentials, routes::credentials::create_credential, routes::credentials::delete_credential, + routes::inference::list_inference_providers, + routes::inference::get_inference_provider_password, + routes::inference::create_inference_provider, + routes::inference::update_inference_provider, + routes::inference::delete_inference_provider, routes::skills::open_skill_folder, routes::skills::edit_skill_folder, routes::skills::get_skill_content, diff --git a/crates/api/src/main.rs b/crates/api/src/main.rs index fc3c1a59..4ef6d605 100644 --- a/crates/api/src/main.rs +++ b/crates/api/src/main.rs @@ -2,7 +2,5 @@ use aghub_api::{start, ApiOptions}; #[tokio::main] async fn main() { - start(ApiOptions { port: 8000 }) - .await - .expect("server error"); + start(ApiOptions::new(8000)).await.expect("server error"); } diff --git a/crates/api/src/routes/inference.rs b/crates/api/src/routes/inference.rs new file mode 100644 index 00000000..2f68be1d --- /dev/null +++ b/crates/api/src/routes/inference.rs @@ -0,0 +1,112 @@ +use aghub_inference::{ + InferenceProvider, InferenceProviderRepository, InferenceProviderStore, +}; +use rocket::http::Status; +use rocket::response::status::NoContent; +use rocket::serde::json::Json; +use rocket::State; + +use crate::dto::inference::{ + CreateInferenceProviderRequest, InferenceProviderPasswordResponse, + InferenceProviderResponse, UpdateInferenceProviderRequest, +}; +use crate::error::{ApiCreated, ApiError, ApiNoContent, ApiResult}; +use crate::state::InferenceProviderState; + +fn store(state: &State) -> InferenceProviderStore { + InferenceProviderStore::new(state.app_data_dir.clone()) +} + +fn find_by_name( + store: &InferenceProviderStore, + name: &str, +) -> Result { + store + .list() + .map_err(ApiError::from)? + .into_iter() + .find(|provider| provider.name.eq_ignore_ascii_case(name)) + .ok_or_else(|| { + ApiError::new( + Status::NotFound, + format!("inference provider '{name}' not found"), + "RESOURCE_NOT_FOUND", + ) + }) +} + +#[get("/inference/providers")] +pub fn list_inference_providers( + state: &State, +) -> ApiResult> { + let providers = store(state) + .list() + .map_err(ApiError::from)? + .into_iter() + .map(InferenceProviderResponse::from) + .collect(); + Ok(Json(providers)) +} + +#[get("/inference/providers//password")] +pub fn get_inference_provider_password( + state: &State, + name: &str, +) -> ApiResult { + let store = store(state); + let provider = find_by_name(&store, name)?; + let api_key = store + .get_api_key(&provider.id) + .map_err(ApiError::from)? + .ok_or_else(|| { + ApiError::new( + Status::NotFound, + format!( + "inference provider '{}' has no stored API key", + provider.name + ), + "RESOURCE_NOT_FOUND", + ) + })?; + + Ok(Json(InferenceProviderPasswordResponse { + name: provider.name, + api_key, + })) +} + +#[post("/inference/providers", data = "")] +pub fn create_inference_provider( + state: &State, + body: Json, +) -> ApiCreated { + let provider = store(state) + .create(body.into_inner().into()) + .map_err(ApiError::from)?; + Ok((Status::Created, Json(provider.into()))) +} + +#[put("/inference/providers/", data = "")] +pub fn update_inference_provider( + state: &State, + name: &str, + body: Json, +) -> ApiResult { + let store = store(state); + let provider = find_by_name(&store, name)?; + let updated = store + .update(&provider.id, body.into_inner().into()) + .map_err(ApiError::from)?; + Ok(Json(updated.into())) +} + +#[delete("/inference/providers/")] +pub fn delete_inference_provider( + state: &State, + name: &str, +) -> ApiNoContent { + let store = store(state); + let provider = find_by_name(&store, name)?; + store.delete(&provider.id).map_err(ApiError::from)?; + Ok(NoContent) +} diff --git a/crates/api/src/routes/mod.rs b/crates/api/src/routes/mod.rs index 7e214a42..fc8eb6c2 100644 --- a/crates/api/src/routes/mod.rs +++ b/crates/api/src/routes/mod.rs @@ -1,6 +1,7 @@ pub mod agents; pub mod catchers; pub mod credentials; +pub mod inference; pub mod integrations; pub mod market; pub mod mcps; diff --git a/crates/api/src/state.rs b/crates/api/src/state.rs index f99cc073..85a519d9 100644 --- a/crates/api/src/state.rs +++ b/crates/api/src/state.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::path::PathBuf; use std::sync::Mutex; use std::time::Instant; use tempfile::TempDir; @@ -19,3 +20,7 @@ pub struct GitCloneSession { pub struct GitCloneSessions { pub sessions: Mutex>, } + +pub struct InferenceProviderState { + pub app_data_dir: PathBuf, +} diff --git a/crates/desktop/src-tauri/src/commands/server.rs b/crates/desktop/src-tauri/src/commands/server.rs index 2c3b627e..7f12ccfa 100644 --- a/crates/desktop/src-tauri/src/commands/server.rs +++ b/crates/desktop/src-tauri/src/commands/server.rs @@ -1,6 +1,7 @@ use crate::AppState; use aghub_api::{start, ApiOptions}; use log::{debug, error, info}; +use tauri::Manager; fn find_available_port() -> Result { let listener = std::net::TcpListener::bind("127.0.0.1:0") @@ -12,12 +13,19 @@ fn find_available_port() -> Result { #[tauri::command] pub async fn start_server( state: tauri::State<'_, AppState>, + app: tauri::AppHandle, ) -> Result { let port = find_available_port()?; + let app_data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; info!("received request to start embedded API server on port {port}"); tokio::spawn(async move { info!("starting embedded API server on 127.0.0.1:{port}"); - if let Err(error) = start(ApiOptions { port }).await { + if let Err(error) = start(ApiOptions { + port, + app_data_dir: Some(app_data_dir), + }) + .await + { error!("embedded API server exited with error: {error}"); } }); diff --git a/crates/desktop/src/generated/dto/CreateInferenceProviderRequest.ts b/crates/desktop/src/generated/dto/CreateInferenceProviderRequest.ts new file mode 100644 index 00000000..9f8982bf --- /dev/null +++ b/crates/desktop/src/generated/dto/CreateInferenceProviderRequest.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { InferenceProviderFormatDto } from "./InferenceProviderFormatDto"; + +export type CreateInferenceProviderRequest = { + name: string; + format: InferenceProviderFormatDto; + api_key: string; +}; diff --git a/crates/desktop/src/generated/dto/InferenceProviderFormatDto.ts b/crates/desktop/src/generated/dto/InferenceProviderFormatDto.ts new file mode 100644 index 00000000..5732c6f5 --- /dev/null +++ b/crates/desktop/src/generated/dto/InferenceProviderFormatDto.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type InferenceProviderFormatDto = + | "anthropic" + | "openai_completions" + | "openai_responses"; diff --git a/crates/desktop/src/generated/dto/InferenceProviderPasswordResponse.ts b/crates/desktop/src/generated/dto/InferenceProviderPasswordResponse.ts new file mode 100644 index 00000000..b1d4ac06 --- /dev/null +++ b/crates/desktop/src/generated/dto/InferenceProviderPasswordResponse.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type InferenceProviderPasswordResponse = { + name: string; + api_key: string; +}; diff --git a/crates/desktop/src/generated/dto/InferenceProviderResponse.ts b/crates/desktop/src/generated/dto/InferenceProviderResponse.ts new file mode 100644 index 00000000..2528e308 --- /dev/null +++ b/crates/desktop/src/generated/dto/InferenceProviderResponse.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { InferenceProviderFormatDto } from "./InferenceProviderFormatDto"; + +export type InferenceProviderResponse = { + id: string; + name: string; + format: InferenceProviderFormatDto; +}; diff --git a/crates/desktop/src/generated/dto/UpdateInferenceProviderRequest.ts b/crates/desktop/src/generated/dto/UpdateInferenceProviderRequest.ts new file mode 100644 index 00000000..fda3b4c7 --- /dev/null +++ b/crates/desktop/src/generated/dto/UpdateInferenceProviderRequest.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { InferenceProviderFormatDto } from "./InferenceProviderFormatDto"; + +export type UpdateInferenceProviderRequest = { + name: string | null; + format: InferenceProviderFormatDto | null; + api_key: string | null; +}; diff --git a/crates/desktop/src/generated/dto/index.ts b/crates/desktop/src/generated/dto/index.ts index 3b0d7935..cac5c313 100644 --- a/crates/desktop/src/generated/dto/index.ts +++ b/crates/desktop/src/generated/dto/index.ts @@ -4,6 +4,7 @@ export type { CapabilitiesDto } from "./CapabilitiesDto"; export type { CodeEditorType } from "./CodeEditorType"; export type { ConfigSource } from "./ConfigSource"; export type { CreateCredentialRequest } from "./CreateCredentialRequest"; +export type { CreateInferenceProviderRequest } from "./CreateInferenceProviderRequest"; export type { CreateMcpRequest } from "./CreateMcpRequest"; export type { CreateSkillRequest } from "./CreateSkillRequest"; export type { CreateSubAgentRequest } from "./CreateSubAgentRequest"; @@ -21,6 +22,9 @@ export type { GitSyncRequest } from "./GitSyncRequest"; export type { GitSyncResponse } from "./GitSyncResponse"; export type { GlobalSkillLockResponse } from "./GlobalSkillLockResponse"; export type { ImportSkillRequest } from "./ImportSkillRequest"; +export type { InferenceProviderFormatDto } from "./InferenceProviderFormatDto"; +export type { InferenceProviderPasswordResponse } from "./InferenceProviderPasswordResponse"; +export type { InferenceProviderResponse } from "./InferenceProviderResponse"; export type { InstallScopeDto } from "./InstallScopeDto"; export type { InstallSkillRequest } from "./InstallSkillRequest"; export type { InstallSkillResponse } from "./InstallSkillResponse"; @@ -53,6 +57,7 @@ export type { ToolInfoDto } from "./ToolInfoDto"; export type { ToolPreferencesDto } from "./ToolPreferencesDto"; export type { TransferRequest } from "./TransferRequest"; export type { TransportDto } from "./TransportDto"; +export type { UpdateInferenceProviderRequest } from "./UpdateInferenceProviderRequest"; export type { UpdateMcpRequest } from "./UpdateMcpRequest"; export type { UpdateSkillRequest } from "./UpdateSkillRequest"; export type { UpdateSubAgentRequest } from "./UpdateSubAgentRequest"; diff --git a/crates/desktop/src/lib/api.ts b/crates/desktop/src/lib/api.ts index 88705e58..9c11a63d 100644 --- a/crates/desktop/src/lib/api.ts +++ b/crates/desktop/src/lib/api.ts @@ -4,6 +4,7 @@ import type { AgentInfo, CodeEditorType, CreateCredentialRequest, + CreateInferenceProviderRequest, CreateMcpRequest, CreateSkillRequest, CreateSubAgentRequest, @@ -18,6 +19,8 @@ import type { GitSyncResponse, GlobalSkillLockResponse, ImportSkillRequest, + InferenceProviderPasswordResponse, + InferenceProviderResponse, InstallSkillRequest, InstallSkillResponse, MarketSkill, @@ -30,6 +33,7 @@ import type { SubAgentResponse, ToolInfoDto, TransferRequest, + UpdateInferenceProviderRequest, UpdateMcpRequest, UpdateSubAgentRequest, } from "../generated/dto"; @@ -438,5 +442,41 @@ export function createApi(baseUrl: string) { return client.delete(`credentials/${id}`).then(() => undefined); }, }, + inferenceProviders: { + list(): Promise { + return client.get("inference/providers").json(); + }, + getPassword( + name: string, + ): Promise { + return client + .get( + `inference/providers/${encodeURIComponent(name)}/password`, + ) + .json(); + }, + create( + body: CreateInferenceProviderRequest, + ): Promise { + return client + .post("inference/providers", { json: body }) + .json(); + }, + update( + name: string, + body: UpdateInferenceProviderRequest, + ): Promise { + return client + .put(`inference/providers/${encodeURIComponent(name)}`, { + json: body, + }) + .json(); + }, + delete(name: string): Promise { + return client + .delete(`inference/providers/${encodeURIComponent(name)}`) + .then(() => undefined); + }, + }, }; } diff --git a/crates/desktop/src/requests/keys.ts b/crates/desktop/src/requests/keys.ts index c51be7a8..dc1a4bb9 100644 --- a/crates/desktop/src/requests/keys.ts +++ b/crates/desktop/src/requests/keys.ts @@ -49,6 +49,12 @@ export const queryKeys = { all: () => ["credentials"] as const, list: () => ["credentials", "list"] as const, }, + inferenceProviders: { + all: () => ["inference-providers"] as const, + list: () => ["inference-providers", "list"] as const, + password: (name: string) => + ["inference-providers", "password", name] as const, + }, integrations: { all: () => ["integrations"] as const, codeEditors: () => ["integrations", "code-editors"] as const, From 851cdc8b40bb034b7c89ce0bfb2981bd52085fc1 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sat, 25 Apr 2026 16:38:54 +0800 Subject: [PATCH 03/62] feat(desktop): add inference provider UI --- crates/desktop/src/lib/locales/en.ts | 39 + crates/desktop/src/lib/locales/zh-Hans.ts | 39 + crates/desktop/src/lib/locales/zh-Hant.ts | 39 + crates/desktop/src/pages/settings/index.tsx | 9 + .../settings/inference-providers-panel.tsx | 852 ++++++++++++++++++ .../src/requests/inference-providers.ts | 109 +++ 6 files changed, 1087 insertions(+) create mode 100644 crates/desktop/src/pages/settings/inference-providers-panel.tsx create mode 100644 crates/desktop/src/requests/inference-providers.ts diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index 714c0a38..0d8e6c98 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -91,6 +91,45 @@ export default { codeEditors: "Code Editor", codeEditorsDescription: "Choose your preferred code editor for opening files", + inferenceProviders: "Inference Providers", + searchInferenceProviders: "Search providers...", + refreshInferenceProviders: "Refresh providers", + createInferenceProvider: "Add Provider", + createInferenceProviderDescription: + "Add an inference endpoint and store its API key securely.", + editInferenceProvider: "Edit Provider", + editInferenceProviderDescription: + "Update provider metadata. Leave the API key empty to keep it unchanged.", + deleteInferenceProvider: "Delete Provider", + deleteInferenceProviderConfirm: + 'Delete "{{name}}"? The stored API key will also be removed.', + noInferenceProviders: "No inference providers yet.", + noInferenceProvidersMatch: "No providers match", + providerName: "Name", + providerNamePlaceholder: "e.g., OpenAI", + providerFormat: "Format", + providerApiKey: "API Key", + providerApiKeyPlaceholder: "sk-...", + providerApiKeyEditPlaceholder: "Leave empty to keep current key", + providerApiKeyStored: "Stored in the native credential store.", + providerApiKeyCopied: "API key copied", + providerApiKeyCopyFailed: "Failed to copy API key", + copyProviderApiKey: "Copy API key", + revealProviderApiKey: "Reveal API key", + hideProviderApiKey: "Hide API key", + inferenceProviderCreated: "Provider created", + inferenceProviderUpdated: "Provider updated", + inferenceProviderDeleted: "Provider deleted", + deleteInferenceProviderError: "Failed to delete provider", + inferenceProviderPasswordLoadFailed: "Failed to load API key", + validationProviderNameRequired: "Enter a provider name.", + validationProviderApiKeyRequired: "Enter an API key.", + inferenceFormatAnthropic: "Anthropic", + inferenceFormatAnthropicDescription: "Anthropic Messages API", + inferenceFormatOpenAiCompletions: "OpenAI Completions", + inferenceFormatOpenAiCompletionsDescription: "OpenAI Chat Completions API", + inferenceFormatOpenAiResponses: "OpenAI Responses", + inferenceFormatOpenAiResponsesDescription: "OpenAI Responses API", // Application application: "About", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index 116e36b2..aa7b0b17 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -87,6 +87,45 @@ export default { integrations: "集成", codeEditors: "代码编辑器", codeEditorsDescription: "选择用于打开文件的首选代码编辑器", + inferenceProviders: "推理 Provider", + searchInferenceProviders: "搜索 Provider...", + refreshInferenceProviders: "刷新 Provider", + createInferenceProvider: "添加 Provider", + createInferenceProviderDescription: + "添加一个推理端点,并安全存储它的 API key。", + editInferenceProvider: "编辑 Provider", + editInferenceProviderDescription: + "更新 Provider 信息。API key 留空则保持不变。", + deleteInferenceProvider: "删除 Provider", + deleteInferenceProviderConfirm: + '确定要删除"{{name}}"吗?已存储的 API key 也会被移除。', + noInferenceProviders: "暂无推理 Provider。", + noInferenceProvidersMatch: "没有匹配的 Provider", + providerName: "名称", + providerNamePlaceholder: "例如:OpenAI", + providerFormat: "格式", + providerApiKey: "API Key", + providerApiKeyPlaceholder: "sk-...", + providerApiKeyEditPlaceholder: "留空以保持当前 key", + providerApiKeyStored: "存储在系统原生凭据库中。", + providerApiKeyCopied: "API key 已复制", + providerApiKeyCopyFailed: "复制 API key 失败", + copyProviderApiKey: "复制 API key", + revealProviderApiKey: "显示 API key", + hideProviderApiKey: "隐藏 API key", + inferenceProviderCreated: "Provider 已创建", + inferenceProviderUpdated: "Provider 已更新", + inferenceProviderDeleted: "Provider 已删除", + deleteInferenceProviderError: "删除 Provider 失败", + inferenceProviderPasswordLoadFailed: "读取 API key 失败", + validationProviderNameRequired: "请输入 Provider 名称。", + validationProviderApiKeyRequired: "请输入 API key。", + inferenceFormatAnthropic: "Anthropic", + inferenceFormatAnthropicDescription: "Anthropic Messages API", + inferenceFormatOpenAiCompletions: "OpenAI Completions", + inferenceFormatOpenAiCompletionsDescription: "OpenAI Chat Completions API", + inferenceFormatOpenAiResponses: "OpenAI Responses", + inferenceFormatOpenAiResponsesDescription: "OpenAI Responses API", // Application application: "关于", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index 54070b0d..bb7c445a 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -87,6 +87,45 @@ export default { integrations: "整合", codeEditors: "程式碼編輯器", codeEditorsDescription: "選擇用於開啟檔案的偏好程式碼編輯器", + inferenceProviders: "推理 Provider", + searchInferenceProviders: "搜尋 Provider...", + refreshInferenceProviders: "重新整理 Provider", + createInferenceProvider: "新增 Provider", + createInferenceProviderDescription: + "新增一個推理端點,並安全儲存它的 API key。", + editInferenceProvider: "編輯 Provider", + editInferenceProviderDescription: + "更新 Provider 資訊。API key 留空則保持不變。", + deleteInferenceProvider: "刪除 Provider", + deleteInferenceProviderConfirm: + "確定要刪除「{{name}}」嗎?已儲存的 API key 也會被移除。", + noInferenceProviders: "尚無推理 Provider。", + noInferenceProvidersMatch: "沒有符合的 Provider", + providerName: "名稱", + providerNamePlaceholder: "例如:OpenAI", + providerFormat: "格式", + providerApiKey: "API Key", + providerApiKeyPlaceholder: "sk-...", + providerApiKeyEditPlaceholder: "留空以保持目前 key", + providerApiKeyStored: "儲存在系統原生憑證庫中。", + providerApiKeyCopied: "API key 已複製", + providerApiKeyCopyFailed: "複製 API key 失敗", + copyProviderApiKey: "複製 API key", + revealProviderApiKey: "顯示 API key", + hideProviderApiKey: "隱藏 API key", + inferenceProviderCreated: "Provider 已建立", + inferenceProviderUpdated: "Provider 已更新", + inferenceProviderDeleted: "Provider 已刪除", + deleteInferenceProviderError: "刪除 Provider 失敗", + inferenceProviderPasswordLoadFailed: "讀取 API key 失敗", + validationProviderNameRequired: "請輸入 Provider 名稱。", + validationProviderApiKeyRequired: "請輸入 API key。", + inferenceFormatAnthropic: "Anthropic", + inferenceFormatAnthropicDescription: "Anthropic Messages API", + inferenceFormatOpenAiCompletions: "OpenAI Completions", + inferenceFormatOpenAiCompletionsDescription: "OpenAI Chat Completions API", + inferenceFormatOpenAiResponses: "OpenAI Responses", + inferenceFormatOpenAiResponsesDescription: "OpenAI Responses API", // Application application: "關於", diff --git a/crates/desktop/src/pages/settings/index.tsx b/crates/desktop/src/pages/settings/index.tsx index 9e7f41e2..fec903bd 100644 --- a/crates/desktop/src/pages/settings/index.tsx +++ b/crates/desktop/src/pages/settings/index.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import AgentsPanel from "./agents-panel"; import AppearancePanel from "./appearance-panel"; import ApplicationPanel from "./application-panel"; +import InferenceProvidersPanel from "./inference-providers-panel"; import IntegrationsPanel from "./integrations-panel"; export default function SettingsPage() { @@ -43,6 +44,10 @@ export default function SettingsPage() { {t("integrations")} + + {t("inferenceProviders")} + + {t("application")} @@ -63,6 +68,10 @@ export default function SettingsPage() { + + + + diff --git a/crates/desktop/src/pages/settings/inference-providers-panel.tsx b/crates/desktop/src/pages/settings/inference-providers-panel.tsx new file mode 100644 index 00000000..a19d3e8e --- /dev/null +++ b/crates/desktop/src/pages/settings/inference-providers-panel.tsx @@ -0,0 +1,852 @@ +import { + ArrowPathIcon, + ClipboardDocumentIcon, + EyeIcon, + EyeSlashIcon, + KeyIcon, + PencilIcon, + PlusIcon, + TrashIcon, +} from "@heroicons/react/24/solid"; +import { + Alert, + AlertDialog, + Button, + Card, + Chip, + FieldError, + Fieldset, + Form, + Input, + Label, + ListBox, + Select, + Spinner, + TextField, + Tooltip, + toast, +} from "@heroui/react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMemo, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { ListSearchHeader } from "../../components/list-search-header"; +import type { + InferenceProviderFormatDto, + InferenceProviderResponse, +} from "../../generated/dto"; +import { useApi } from "../../hooks/use-api"; +import { cn } from "../../lib/utils"; +import { + createInferenceProviderMutationOptions, + deleteInferenceProviderMutationOptions, + inferenceProviderListQueryOptions, + updateInferenceProviderMutationOptions, +} from "../../requests/inference-providers"; + +type PanelMode = + | { type: "detail" } + | { type: "create" } + | { type: "edit"; provider: InferenceProviderResponse }; + +interface FormatOption { + id: InferenceProviderFormatDto; + labelKey: string; + descriptionKey: string; +} + +const FORMAT_OPTIONS: FormatOption[] = [ + { + id: "anthropic", + labelKey: "inferenceFormatAnthropic", + descriptionKey: "inferenceFormatAnthropicDescription", + }, + { + id: "openai_completions", + labelKey: "inferenceFormatOpenAiCompletions", + descriptionKey: "inferenceFormatOpenAiCompletionsDescription", + }, + { + id: "openai_responses", + labelKey: "inferenceFormatOpenAiResponses", + descriptionKey: "inferenceFormatOpenAiResponsesDescription", + }, +]; + +interface InferenceProviderFormValues { + name: string; + format: InferenceProviderFormatDto; + apiKey: string; +} + +function formatOption( + format: InferenceProviderFormatDto, + t: (key: string) => string, +) { + const option = FORMAT_OPTIONS.find((item) => item.id === format); + return option ? t(option.labelKey) : format; +} + +function formatDescription( + format: InferenceProviderFormatDto, + t: (key: string) => string, +) { + const option = FORMAT_OPTIONS.find((item) => item.id === format); + return option ? t(option.descriptionKey) : format; +} + +function ProviderIcon({ + format, + isActive = false, +}: { + format: InferenceProviderFormatDto; + isActive?: boolean; +}) { + return ( +
+ + {format} +
+ ); +} + +function ProviderForm({ + mode, + provider, + onCancel, + onSuccess, +}: { + mode: "create" | "edit"; + provider?: InferenceProviderResponse; + onCancel: () => void; + onSuccess: (provider: InferenceProviderResponse) => void; +}) { + const { t } = useTranslation(); + const api = useApi(); + const queryClient = useQueryClient(); + const { + control, + handleSubmit, + formState: { isSubmitting }, + } = useForm({ + mode: "onSubmit", + reValidateMode: "onChange", + defaultValues: { + name: provider?.name ?? "", + format: provider?.format ?? "openai_responses", + apiKey: "", + }, + }); + + const createMutation = useMutation({ + ...createInferenceProviderMutationOptions({ + api, + queryClient, + }), + }); + const updateMutation = useMutation({ + ...updateInferenceProviderMutationOptions({ + api, + queryClient, + }), + }); + + const activeError = + mode === "create" ? createMutation.error : updateMutation.error; + const isPending = + createMutation.isPending || updateMutation.isPending || isSubmitting; + + const onSubmit = async (values: InferenceProviderFormValues) => { + const name = values.name.trim(); + const apiKey = values.apiKey.trim(); + + try { + if (mode === "create") { + const created = await createMutation.mutateAsync({ + name, + format: values.format, + api_key: apiKey, + }); + toast.success(t("inferenceProviderCreated")); + onSuccess(created); + return; + } + + if (!provider) return; + const updated = await updateMutation.mutateAsync({ + name: provider.name, + body: { + name, + format: values.format, + api_key: apiKey || null, + }, + }); + toast.success(t("inferenceProviderUpdated")); + onSuccess(updated); + } catch (error) { + console.error("Failed to save inference provider:", error); + } + }; + + return ( +
+ {activeError && ( + + + + + {activeError instanceof Error + ? activeError.message + : String(activeError)} + + + + )} + + + +
+ + {mode === "create" + ? t("createInferenceProvider") + : t("editInferenceProvider")} + + + {mode === "create" + ? t("createInferenceProviderDescription") + : t("editInferenceProviderDescription")} + +
+
+ +
+
+ + + value.trim() + ? true + : t( + "validationProviderNameRequired", + ), + }} + render={({ field, fieldState }) => ( + + + + field.onChange( + event.target.value, + ) + } + onBlur={field.onBlur} + placeholder={t( + "providerNamePlaceholder", + )} + variant="secondary" + /> + {fieldState.error && ( + + {fieldState.error.message} + + )} + + )} + /> + + ( + + )} + /> + + { + if (mode === "edit") return true; + return value.trim() + ? true + : t( + "validationProviderApiKeyRequired", + ); + }, + }} + render={({ field, fieldState }) => ( + + + + field.onChange( + event.target.value, + ) + } + onBlur={field.onBlur} + placeholder={ + mode === "create" + ? t( + "providerApiKeyPlaceholder", + ) + : t( + "providerApiKeyEditPlaceholder", + ) + } + variant="secondary" + /> + {fieldState.error && ( + + {fieldState.error.message} + + )} + + )} + /> + +
+ +
+ + +
+
+
+
+
+ ); +} + +function ProviderDetail({ + provider, + onEdit, + onDeleted, +}: { + provider: InferenceProviderResponse; + onEdit: () => void; + onDeleted: () => void; +}) { + const { t } = useTranslation(); + const api = useApi(); + const queryClient = useQueryClient(); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); + const [revealedKey, setRevealedKey] = useState(null); + const [isCopying, setIsCopying] = useState(false); + + const passwordMutation = useMutation({ + mutationFn: (name: string) => api.inferenceProviders.getPassword(name), + onSuccess: (data) => { + setRevealedKey(data.api_key); + }, + onError: (error) => { + console.error("Failed to load inference provider key:", error); + toast.danger( + error instanceof Error + ? error.message + : t("inferenceProviderPasswordLoadFailed"), + ); + }, + }); + + const deleteMutation = useMutation({ + ...deleteInferenceProviderMutationOptions({ + api, + queryClient, + onSuccess: async () => { + setIsDeleteOpen(false); + toast.success(t("inferenceProviderDeleted")); + onDeleted(); + }, + }), + onError: (error) => { + console.error("Failed to delete inference provider:", error); + toast.danger( + error instanceof Error + ? error.message + : t("deleteInferenceProviderError"), + ); + }, + }); + + const handleReveal = () => { + if (revealedKey) { + setRevealedKey(null); + return; + } + passwordMutation.mutate(provider.name); + }; + + const handleCopyKey = async () => { + setIsCopying(true); + try { + const password = revealedKey + ? { api_key: revealedKey } + : await api.inferenceProviders.getPassword(provider.name); + await navigator.clipboard.writeText(password.api_key); + toast.success(t("providerApiKeyCopied")); + } catch (error) { + console.error("Failed to copy inference provider key:", error); + toast.danger( + error instanceof Error + ? error.message + : t("providerApiKeyCopyFailed"), + ); + } finally { + setIsCopying(false); + } + }; + + return ( +
+
+
+ +
+

+ {provider.name} +

+

+ {formatOption(provider.format, t)} +

+
+
+
+ + + + + {t("edit")} + + + + + + {t("delete")} + +
+
+ +
+ + +
+ {t("providerFormat")} + + {formatDescription(provider.format, t)} + +
+ + {formatOption(provider.format, t)} + +
+
+ + + +
+ {t("providerApiKey")} + + {t("providerApiKeyStored")} + +
+
+ +
+ + {revealedKey ?? "••••••••••••••••••••••••"} + + + +
+
+
+
+ + + + + + + + + {t("deleteInferenceProvider")} + + + + {t("deleteInferenceProviderConfirm", { + name: provider.name, + })} + + + + + + + + +
+ ); +} + +export default function InferenceProvidersPanel() { + const { t } = useTranslation(); + const api = useApi(); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedName, setSelectedName] = useState(null); + const [panel, setPanel] = useState({ type: "detail" }); + + const { + data: providers = [], + isLoading, + isFetching, + refetch, + } = useQuery({ + ...inferenceProviderListQueryOptions({ api }), + }); + + const filteredProviders = useMemo(() => { + const query = searchQuery.trim().toLowerCase(); + if (!query) return providers; + + return providers.filter((provider) => { + const format = formatOption(provider.format, t).toLowerCase(); + return ( + provider.name.toLowerCase().includes(query) || + provider.format.includes(query) || + format.includes(query) + ); + }); + }, [providers, searchQuery, t]); + + const activeProvider = useMemo(() => { + if (selectedName) { + const selected = providers.find( + (provider) => provider.name === selectedName, + ); + if (selected) return selected; + } + return providers[0] ?? null; + }, [providers, selectedName]); + + const selectedKeys = useMemo(() => { + return activeProvider && panel.type !== "create" + ? new Set([activeProvider.name]) + : new Set(); + }, [activeProvider, panel.type]); + + const handleCreatedOrUpdated = (provider: InferenceProviderResponse) => { + setSelectedName(provider.name); + setPanel({ type: "detail" }); + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+ + + + + + {t("add")} + + + + + + {t("refresh")} + + + + {filteredProviders.length === 0 ? ( +
+

+ {providers.length === 0 + ? t("noInferenceProviders") + : t("noInferenceProvidersMatch")} +

+
+ ) : ( +
+ { + setSelectedName(String(key)); + setPanel({ type: "detail" }); + }} + className="p-2" + > + {filteredProviders.map((provider) => ( + +
+ +
+ + + {formatOption( + provider.format, + t, + )} + +
+
+
+ ))} +
+
+ )} +
+ +
+ {panel.type === "create" && ( + setPanel({ type: "detail" })} + onSuccess={handleCreatedOrUpdated} + /> + )} + + {panel.type === "edit" && ( + setPanel({ type: "detail" })} + onSuccess={handleCreatedOrUpdated} + /> + )} + + {panel.type === "detail" && activeProvider && ( + + setPanel({ + type: "edit", + provider: activeProvider, + }) + } + onDeleted={() => { + setSelectedName(null); + setPanel({ type: "detail" }); + }} + /> + )} + + {panel.type === "detail" && !activeProvider && ( +
+
+

+ {t("noInferenceProviders")} +

+
+ +
+ )} +
+
+
+ ); +} diff --git a/crates/desktop/src/requests/inference-providers.ts b/crates/desktop/src/requests/inference-providers.ts new file mode 100644 index 00000000..7d3dba63 --- /dev/null +++ b/crates/desktop/src/requests/inference-providers.ts @@ -0,0 +1,109 @@ +import { + mutationOptions, + type QueryClient, + queryOptions, +} from "@tanstack/react-query"; +import type { + CreateInferenceProviderRequest, + InferenceProviderResponse, + UpdateInferenceProviderRequest, +} from "../generated/dto"; +import type { ApiClient } from "./client"; +import { queryKeys } from "./keys"; + +interface InferenceProviderListQueryParams { + api: ApiClient; + enabled?: boolean; + staleTime?: number; +} + +export function inferenceProviderListQueryOptions({ + api, + enabled = true, + staleTime = 30_000, +}: InferenceProviderListQueryParams) { + return queryOptions({ + queryKey: queryKeys.inferenceProviders.list(), + queryFn: () => api.inferenceProviders.list(), + enabled, + staleTime, + }); +} + +export async function invalidateInferenceProviderQueries( + queryClient: QueryClient, +) { + await queryClient.invalidateQueries({ + queryKey: queryKeys.inferenceProviders.all(), + }); +} + +interface CreateInferenceProviderMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: (data: InferenceProviderResponse) => void | Promise; +} + +export function createInferenceProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: CreateInferenceProviderMutationParams) { + return mutationOptions({ + mutationFn: (body: CreateInferenceProviderRequest) => + api.inferenceProviders.create(body), + onSuccess: async (data) => { + await invalidateInferenceProviderQueries(queryClient); + await onSuccess?.(data); + }, + }); +} + +interface UpdateInferenceProviderVariables { + name: string; + body: UpdateInferenceProviderRequest; +} + +interface UpdateInferenceProviderMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: ( + data: InferenceProviderResponse, + variables: UpdateInferenceProviderVariables, + ) => void | Promise; +} + +export function updateInferenceProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: UpdateInferenceProviderMutationParams) { + return mutationOptions({ + mutationFn: ({ name, body }: UpdateInferenceProviderVariables) => + api.inferenceProviders.update(name, body), + onSuccess: async (data, variables) => { + await invalidateInferenceProviderQueries(queryClient); + await onSuccess?.(data, variables); + }, + }); +} + +interface DeleteInferenceProviderMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: () => void | Promise; +} + +export function deleteInferenceProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: DeleteInferenceProviderMutationParams) { + return mutationOptions({ + mutationFn: (name: string) => api.inferenceProviders.delete(name), + onSuccess: async () => { + await invalidateInferenceProviderQueries(queryClient); + await onSuccess?.(); + }, + }); +} From 57b1e32121ce45fe9bc5062b36472cfd7af206bf Mon Sep 17 00:00:00 2001 From: akarachen Date: Sat, 25 Apr 2026 16:53:09 +0800 Subject: [PATCH 04/62] fix(desktop): move providers to sidebar --- crates/desktop/src/App.tsx | 8 + crates/desktop/src/lib/sidebar-navigation.ts | 7 + crates/desktop/src/lib/store/types.ts | 1 + ...ders-panel.tsx => inference-providers.tsx} | 304 +++++++++--------- crates/desktop/src/pages/settings/index.tsx | 9 - 5 files changed, 166 insertions(+), 163 deletions(-) rename crates/desktop/src/pages/{settings/inference-providers-panel.tsx => inference-providers.tsx} (79%) diff --git a/crates/desktop/src/App.tsx b/crates/desktop/src/App.tsx index a4b3a8b1..2efe0d68 100644 --- a/crates/desktop/src/App.tsx +++ b/crates/desktop/src/App.tsx @@ -20,6 +20,7 @@ import type { DeepLinkImportIntent } from "./lib/deep-link"; import { parseDeepLink } from "./lib/deep-link"; import { setupAppMenu } from "./lib/menu"; import { initStore } from "./lib/store"; +import InferenceProvidersPage from "./pages/inference-providers"; import ProjectDetailPage from "./pages/project/detail"; import SettingsPage from "./pages/settings"; import CustomAgentsPage from "./pages/settings/custom-agents"; @@ -204,6 +205,13 @@ function App() { + + + + + + + diff --git a/crates/desktop/src/lib/sidebar-navigation.ts b/crates/desktop/src/lib/sidebar-navigation.ts index be9961d9..7341cd6a 100644 --- a/crates/desktop/src/lib/sidebar-navigation.ts +++ b/crates/desktop/src/lib/sidebar-navigation.ts @@ -1,6 +1,7 @@ import { BookOpenIcon, CpuChipIcon, + KeyIcon, ServerIcon, SquaresPlusIcon, } from "@heroicons/react/24/solid"; @@ -33,6 +34,12 @@ const SIDEBAR_ITEM_DEFINITIONS: Record = { icon: ServerIcon, tour: "nav-mcp", }, + inferenceProviders: { + id: "inferenceProviders", + labelKey: "inferenceProviders", + href: "/inference-providers", + icon: KeyIcon, + }, skills: { id: "skills", labelKey: "skills", diff --git a/crates/desktop/src/lib/store/types.ts b/crates/desktop/src/lib/store/types.ts index 6ed662f9..4ab6b16f 100644 --- a/crates/desktop/src/lib/store/types.ts +++ b/crates/desktop/src/lib/store/types.ts @@ -20,6 +20,7 @@ export interface IntegrationPreferences { export const SIDEBAR_ITEM_IDS = [ "mcp", + "inferenceProviders", "skills", "skillsSh", "subAgents", diff --git a/crates/desktop/src/pages/settings/inference-providers-panel.tsx b/crates/desktop/src/pages/inference-providers.tsx similarity index 79% rename from crates/desktop/src/pages/settings/inference-providers-panel.tsx rename to crates/desktop/src/pages/inference-providers.tsx index a19d3e8e..88b0ff4e 100644 --- a/crates/desktop/src/pages/settings/inference-providers-panel.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -30,19 +30,19 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMemo, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import { ListSearchHeader } from "../../components/list-search-header"; +import { ListSearchHeader } from "../components/list-search-header"; import type { InferenceProviderFormatDto, InferenceProviderResponse, -} from "../../generated/dto"; -import { useApi } from "../../hooks/use-api"; -import { cn } from "../../lib/utils"; +} from "../generated/dto"; +import { useApi } from "../hooks/use-api"; +import { cn } from "../lib/utils"; import { createInferenceProviderMutationOptions, deleteInferenceProviderMutationOptions, inferenceProviderListQueryOptions, updateInferenceProviderMutationOptions, -} from "../../requests/inference-providers"; +} from "../requests/inference-providers"; type PanelMode = | { type: "detail" } @@ -633,7 +633,7 @@ function ProviderDetail({ ); } -export default function InferenceProvidersPanel() { +export default function InferenceProvidersPage() { const { t } = useTranslation(); const api = useApi(); const [searchQuery, setSearchQuery] = useState(""); @@ -686,166 +686,162 @@ export default function InferenceProvidersPanel() { if (isLoading) { return ( -
+
); } return ( -
-
-
- - - - - - {t("add")} - - - - - - {t("refresh")} - - - - {filteredProviders.length === 0 ? ( -
-

- {providers.length === 0 - ? t("noInferenceProviders") - : t("noInferenceProvidersMatch")} -

-
- ) : ( -
- { - setSelectedName(String(key)); - setPanel({ type: "detail" }); +
+
+ + + +
- )} -
- -
- {panel.type === "create" && ( - setPanel({ type: "detail" })} - onSuccess={handleCreatedOrUpdated} - /> - )} - - {panel.type === "edit" && ( - setPanel({ type: "detail" })} - onSuccess={handleCreatedOrUpdated} - /> - )} - - {panel.type === "detail" && activeProvider && ( - - setPanel({ - type: "edit", - provider: activeProvider, - }) - } - onDeleted={() => { - setSelectedName(null); - setPanel({ type: "detail" }); - }} - /> - )} - - {panel.type === "detail" && !activeProvider && ( -
-
-

- {t("noInferenceProviders")} -

-
+ + + + {t("add")} + + + + + {t("refresh")} + + + + {filteredProviders.length === 0 ? ( +
+

+ {providers.length === 0 + ? t("noInferenceProviders") + : t("noInferenceProvidersMatch")} +

+
+ ) : ( +
+ { + setSelectedName(String(key)); + setPanel({ type: "detail" }); + }} + className="p-2" + > + {filteredProviders.map((provider) => ( + +
+ +
+ + + {formatOption( + provider.format, + t, + )} + +
+
+
+ ))} +
+
+ )} +
+ +
+ {panel.type === "create" && ( + setPanel({ type: "detail" })} + onSuccess={handleCreatedOrUpdated} + /> + )} + + {panel.type === "edit" && ( + setPanel({ type: "detail" })} + onSuccess={handleCreatedOrUpdated} + /> + )} + + {panel.type === "detail" && activeProvider && ( + + setPanel({ + type: "edit", + provider: activeProvider, + }) + } + onDeleted={() => { + setSelectedName(null); + setPanel({ type: "detail" }); + }} + /> + )} + + {panel.type === "detail" && !activeProvider && ( +
+
+

+ {t("noInferenceProviders")} +

- )} -
+ +
+ )}
); diff --git a/crates/desktop/src/pages/settings/index.tsx b/crates/desktop/src/pages/settings/index.tsx index fec903bd..9e7f41e2 100644 --- a/crates/desktop/src/pages/settings/index.tsx +++ b/crates/desktop/src/pages/settings/index.tsx @@ -4,7 +4,6 @@ import { useTranslation } from "react-i18next"; import AgentsPanel from "./agents-panel"; import AppearancePanel from "./appearance-panel"; import ApplicationPanel from "./application-panel"; -import InferenceProvidersPanel from "./inference-providers-panel"; import IntegrationsPanel from "./integrations-panel"; export default function SettingsPage() { @@ -44,10 +43,6 @@ export default function SettingsPage() { {t("integrations")} - - {t("inferenceProviders")} - - {t("application")} @@ -68,10 +63,6 @@ export default function SettingsPage() { - - - - From 870f5ea880c503b3192decd576d85f0c3206a5de Mon Sep 17 00:00:00 2001 From: akarachen Date: Sat, 25 Apr 2026 17:04:02 +0800 Subject: [PATCH 05/62] feat(inference): add provider base URL --- crates/api/src/dto/inference.rs | 6 ++ crates/api/src/error.rs | 1 + .../dto/CreateInferenceProviderRequest.ts | 1 + .../dto/InferenceProviderResponse.ts | 1 + .../dto/UpdateInferenceProviderRequest.ts | 1 + crates/desktop/src/lib/locales/en.ts | 4 ++ crates/desktop/src/lib/locales/zh-Hans.ts | 4 ++ crates/desktop/src/lib/locales/zh-Hant.ts | 4 ++ .../desktop/src/pages/inference-providers.tsx | 71 +++++++++++++++++++ crates/inference/src/error.rs | 4 ++ crates/inference/src/model.rs | 11 +++ crates/inference/src/store.rs | 42 +++++++++++ 12 files changed, 150 insertions(+) diff --git a/crates/api/src/dto/inference.rs b/crates/api/src/dto/inference.rs index f4558d27..e5ef28dd 100644 --- a/crates/api/src/dto/inference.rs +++ b/crates/api/src/dto/inference.rs @@ -47,6 +47,7 @@ impl From for InferenceProviderFormat { pub struct CreateInferenceProviderRequest { pub name: String, pub format: InferenceProviderFormatDto, + pub api_base_url: String, pub api_key: String, } @@ -55,6 +56,7 @@ impl From for CreateInferenceProvider { CreateInferenceProvider { name: req.name, format: req.format.into(), + api_base_url: req.api_base_url, api_key: req.api_key, } } @@ -65,6 +67,7 @@ impl From for CreateInferenceProvider { pub struct UpdateInferenceProviderRequest { pub name: Option, pub format: Option, + pub api_base_url: Option, pub api_key: Option, } @@ -73,6 +76,7 @@ impl From for UpdateInferenceProvider { UpdateInferenceProvider { name: req.name, format: req.format.map(Into::into), + api_base_url: req.api_base_url, api_key: req.api_key, } } @@ -84,6 +88,7 @@ pub struct InferenceProviderResponse { pub id: String, pub name: String, pub format: InferenceProviderFormatDto, + pub api_base_url: String, } impl From for InferenceProviderResponse { @@ -98,6 +103,7 @@ impl From<&InferenceProvider> for InferenceProviderResponse { id: provider.id.clone(), name: provider.name.clone(), format: provider.format.into(), + api_base_url: provider.api_base_url.clone(), } } } diff --git a/crates/api/src/error.rs b/crates/api/src/error.rs index edddbf0c..d01dcf1d 100644 --- a/crates/api/src/error.rs +++ b/crates/api/src/error.rs @@ -87,6 +87,7 @@ impl From for ApiError { fn from(e: InferenceProviderError) -> Self { match e { InferenceProviderError::EmptyName + | InferenceProviderError::EmptyApiBaseUrl | InferenceProviderError::EmptyApiKey | InferenceProviderError::InvalidFormat(_) => ApiError::new( Status::BadRequest, diff --git a/crates/desktop/src/generated/dto/CreateInferenceProviderRequest.ts b/crates/desktop/src/generated/dto/CreateInferenceProviderRequest.ts index 9f8982bf..23c4ecde 100644 --- a/crates/desktop/src/generated/dto/CreateInferenceProviderRequest.ts +++ b/crates/desktop/src/generated/dto/CreateInferenceProviderRequest.ts @@ -4,5 +4,6 @@ import type { InferenceProviderFormatDto } from "./InferenceProviderFormatDto"; export type CreateInferenceProviderRequest = { name: string; format: InferenceProviderFormatDto; + api_base_url: string; api_key: string; }; diff --git a/crates/desktop/src/generated/dto/InferenceProviderResponse.ts b/crates/desktop/src/generated/dto/InferenceProviderResponse.ts index 2528e308..5d09e028 100644 --- a/crates/desktop/src/generated/dto/InferenceProviderResponse.ts +++ b/crates/desktop/src/generated/dto/InferenceProviderResponse.ts @@ -5,4 +5,5 @@ export type InferenceProviderResponse = { id: string; name: string; format: InferenceProviderFormatDto; + api_base_url: string; }; diff --git a/crates/desktop/src/generated/dto/UpdateInferenceProviderRequest.ts b/crates/desktop/src/generated/dto/UpdateInferenceProviderRequest.ts index fda3b4c7..75d8ae67 100644 --- a/crates/desktop/src/generated/dto/UpdateInferenceProviderRequest.ts +++ b/crates/desktop/src/generated/dto/UpdateInferenceProviderRequest.ts @@ -4,5 +4,6 @@ import type { InferenceProviderFormatDto } from "./InferenceProviderFormatDto"; export type UpdateInferenceProviderRequest = { name: string | null; format: InferenceProviderFormatDto | null; + api_base_url: string | null; api_key: string | null; }; diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index 0d8e6c98..6f858792 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -108,6 +108,9 @@ export default { providerName: "Name", providerNamePlaceholder: "e.g., OpenAI", providerFormat: "Format", + providerApiBaseUrl: "API Base URL", + providerApiBaseUrlPlaceholder: "https://api.openai.com/v1", + providerApiBaseUrlDescription: "Root endpoint used for provider requests.", providerApiKey: "API Key", providerApiKeyPlaceholder: "sk-...", providerApiKeyEditPlaceholder: "Leave empty to keep current key", @@ -123,6 +126,7 @@ export default { deleteInferenceProviderError: "Failed to delete provider", inferenceProviderPasswordLoadFailed: "Failed to load API key", validationProviderNameRequired: "Enter a provider name.", + validationProviderApiBaseUrlRequired: "Enter an API base URL.", validationProviderApiKeyRequired: "Enter an API key.", inferenceFormatAnthropic: "Anthropic", inferenceFormatAnthropicDescription: "Anthropic Messages API", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index aa7b0b17..70b9bf1e 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -104,6 +104,9 @@ export default { providerName: "名称", providerNamePlaceholder: "例如:OpenAI", providerFormat: "格式", + providerApiBaseUrl: "API Base URL", + providerApiBaseUrlPlaceholder: "https://api.openai.com/v1", + providerApiBaseUrlDescription: "用于发起 Provider 请求的根端点。", providerApiKey: "API Key", providerApiKeyPlaceholder: "sk-...", providerApiKeyEditPlaceholder: "留空以保持当前 key", @@ -119,6 +122,7 @@ export default { deleteInferenceProviderError: "删除 Provider 失败", inferenceProviderPasswordLoadFailed: "读取 API key 失败", validationProviderNameRequired: "请输入 Provider 名称。", + validationProviderApiBaseUrlRequired: "请输入 API base URL。", validationProviderApiKeyRequired: "请输入 API key。", inferenceFormatAnthropic: "Anthropic", inferenceFormatAnthropicDescription: "Anthropic Messages API", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index bb7c445a..b1c86a7b 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -104,6 +104,9 @@ export default { providerName: "名稱", providerNamePlaceholder: "例如:OpenAI", providerFormat: "格式", + providerApiBaseUrl: "API Base URL", + providerApiBaseUrlPlaceholder: "https://api.openai.com/v1", + providerApiBaseUrlDescription: "用於發起 Provider 請求的根端點。", providerApiKey: "API Key", providerApiKeyPlaceholder: "sk-...", providerApiKeyEditPlaceholder: "留空以保持目前 key", @@ -119,6 +122,7 @@ export default { deleteInferenceProviderError: "刪除 Provider 失敗", inferenceProviderPasswordLoadFailed: "讀取 API key 失敗", validationProviderNameRequired: "請輸入 Provider 名稱。", + validationProviderApiBaseUrlRequired: "請輸入 API base URL。", validationProviderApiKeyRequired: "請輸入 API key。", inferenceFormatAnthropic: "Anthropic", inferenceFormatAnthropicDescription: "Anthropic Messages API", diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index 88b0ff4e..21152181 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -76,6 +76,7 @@ const FORMAT_OPTIONS: FormatOption[] = [ interface InferenceProviderFormValues { name: string; format: InferenceProviderFormatDto; + apiBaseUrl: string; apiKey: string; } @@ -139,6 +140,7 @@ function ProviderForm({ defaultValues: { name: provider?.name ?? "", format: provider?.format ?? "openai_responses", + apiBaseUrl: provider?.api_base_url ?? "", apiKey: "", }, }); @@ -163,6 +165,7 @@ function ProviderForm({ const onSubmit = async (values: InferenceProviderFormValues) => { const name = values.name.trim(); + const apiBaseUrl = values.apiBaseUrl.trim(); const apiKey = values.apiKey.trim(); try { @@ -170,6 +173,7 @@ function ProviderForm({ const created = await createMutation.mutateAsync({ name, format: values.format, + api_base_url: apiBaseUrl, api_key: apiKey, }); toast.success(t("inferenceProviderCreated")); @@ -183,6 +187,7 @@ function ProviderForm({ body: { name, format: values.format, + api_base_url: apiBaseUrl, api_key: apiKey || null, }, }); @@ -277,6 +282,55 @@ function ProviderForm({ )} /> + + value.trim() + ? true + : t( + "validationProviderApiBaseUrlRequired", + ), + }} + render={({ field, fieldState }) => ( + + + + field.onChange( + event.target.value, + ) + } + onBlur={field.onBlur} + placeholder={t( + "providerApiBaseUrlPlaceholder", + )} + variant="secondary" + /> + {fieldState.error && ( + + {fieldState.error.message} + + )} + + )} + /> + + + +
+ {t("providerApiBaseUrl")} + + {t("providerApiBaseUrlDescription")} + +
+
+ + + {provider.api_base_url} + + +
+
@@ -657,6 +727,7 @@ export default function InferenceProvidersPage() { const format = formatOption(provider.format, t).toLowerCase(); return ( provider.name.toLowerCase().includes(query) || + provider.api_base_url.toLowerCase().includes(query) || provider.format.includes(query) || format.includes(query) ); diff --git a/crates/inference/src/error.rs b/crates/inference/src/error.rs index 020cf58b..f5f1814d 100644 --- a/crates/inference/src/error.rs +++ b/crates/inference/src/error.rs @@ -23,6 +23,10 @@ pub enum InferenceProviderError { #[error("provider API key cannot be empty")] EmptyApiKey, + /// API base URLs must not be empty. + #[error("provider API base URL cannot be empty")] + EmptyApiBaseUrl, + /// Provider name is already in use. #[error("provider already exists: {0}")] AlreadyExists(String), diff --git a/crates/inference/src/model.rs b/crates/inference/src/model.rs index c5fb2117..a05aa24d 100644 --- a/crates/inference/src/model.rs +++ b/crates/inference/src/model.rs @@ -73,6 +73,9 @@ pub struct InferenceProvider { /// Request/response format supported by this provider. pub format: InferenceProviderFormat, + + /// Base URL for provider API requests. + pub api_base_url: String, } /// Provider creation input. @@ -84,6 +87,9 @@ pub struct CreateInferenceProvider { /// Request/response format supported by this provider. pub format: InferenceProviderFormat, + /// Base URL for provider API requests. + pub api_base_url: String, + /// Provider API key. This is write-only and never stored in JSON. pub api_key: String, } @@ -93,6 +99,7 @@ impl fmt::Debug for CreateInferenceProvider { f.debug_struct("CreateInferenceProvider") .field("name", &self.name) .field("format", &self.format) + .field("api_base_url", &self.api_base_url) .field("api_key", &"[redacted]") .finish() } @@ -107,6 +114,9 @@ pub struct UpdateInferenceProvider { /// Updated request/response format. pub format: Option, + /// Updated base URL for provider API requests. + pub api_base_url: Option, + /// Updated provider API key. This is write-only and never stored in JSON. pub api_key: Option, } @@ -116,6 +126,7 @@ impl fmt::Debug for UpdateInferenceProvider { f.debug_struct("UpdateInferenceProvider") .field("name", &self.name) .field("format", &self.format) + .field("api_base_url", &self.api_base_url) .field("api_key", &self.api_key.as_ref().map(|_| "[redacted]")) .finish() } diff --git a/crates/inference/src/store.rs b/crates/inference/src/store.rs index 2114b665..c4f4b72b 100644 --- a/crates/inference/src/store.rs +++ b/crates/inference/src/store.rs @@ -176,6 +176,7 @@ impl InferenceProviderRepository input: CreateInferenceProvider, ) -> Result { let name = clean_name(&input.name)?; + let api_base_url = clean_api_base_url(&input.api_base_url)?; ensure_api_key(&input.api_key)?; let mut file = self.read_file()?; @@ -185,6 +186,7 @@ impl InferenceProviderRepository id: uuid::Uuid::new_v4().to_string(), name, format: input.format, + api_base_url, }; self.credentials.set_api_key(&provider.id, &input.api_key)?; @@ -215,6 +217,11 @@ impl InferenceProviderRepository file.providers[index].format = format; } + if let Some(ref api_base_url) = input.api_base_url { + file.providers[index].api_base_url = + clean_api_base_url(api_base_url)?; + } + let previous_api_key = match input.api_key.as_ref() { Some(api_key) => { ensure_api_key(api_key)?; @@ -293,6 +300,15 @@ fn ensure_api_key(api_key: &str) -> Result<()> { } } +fn clean_api_base_url(api_base_url: &str) -> Result { + let api_base_url = api_base_url.trim(); + if api_base_url.is_empty() { + Err(InferenceProviderError::EmptyApiBaseUrl) + } else { + Ok(api_base_url.to_string()) + } +} + #[cfg(test)] mod tests { use std::collections::HashMap; @@ -352,6 +368,7 @@ mod tests { .create(CreateInferenceProvider { name: "OpenAI".to_string(), format: InferenceProviderFormat::OpenAiResponses, + api_base_url: "https://api.openai.com/v1".to_string(), api_key: "sk-test".to_string(), }) .unwrap(); @@ -359,6 +376,7 @@ mod tests { let contents = fs::read_to_string(store.file_path()).unwrap(); assert!(contents.contains("OpenAI")); assert!(contents.contains("openai_responses")); + assert!(contents.contains("https://api.openai.com/v1")); assert!(!contents.contains("sk-test")); assert_eq!( store.get_api_key(&provider.id).unwrap(), @@ -373,6 +391,7 @@ mod tests { .create(CreateInferenceProvider { name: "Anthropic".to_string(), format: InferenceProviderFormat::Anthropic, + api_base_url: "https://api.anthropic.com/v1".to_string(), api_key: "first-key".to_string(), }) .unwrap(); @@ -383,6 +402,9 @@ mod tests { UpdateInferenceProvider { name: Some("Claude".to_string()), format: Some(InferenceProviderFormat::OpenAiCompletions), + api_base_url: Some( + "https://gateway.example.com/v1".to_string(), + ), api_key: Some("second-key".to_string()), }, ) @@ -390,6 +412,7 @@ mod tests { assert_eq!(updated.name, "Claude"); assert_eq!(updated.format, InferenceProviderFormat::OpenAiCompletions); + assert_eq!(updated.api_base_url, "https://gateway.example.com/v1"); assert_eq!( store.get_api_key(&provider.id).unwrap(), Some("second-key".to_string()) @@ -403,6 +426,7 @@ mod tests { .create(CreateInferenceProvider { name: "Anthropic".to_string(), format: InferenceProviderFormat::Anthropic, + api_base_url: "https://api.anthropic.com/v1".to_string(), api_key: "secret".to_string(), }) .unwrap(); @@ -421,6 +445,7 @@ mod tests { .create(CreateInferenceProvider { name: "OpenAI".to_string(), format: InferenceProviderFormat::OpenAiResponses, + api_base_url: "https://api.openai.com/v1".to_string(), api_key: "first".to_string(), }) .unwrap(); @@ -429,10 +454,27 @@ mod tests { .create(CreateInferenceProvider { name: "openai".to_string(), format: InferenceProviderFormat::OpenAiCompletions, + api_base_url: "https://gateway.example.com/v1".to_string(), api_key: "second".to_string(), }) .unwrap_err(); assert!(matches!(error, InferenceProviderError::AlreadyExists(_))); } + + #[test] + fn test_empty_api_base_url_is_rejected() { + let (_temp, store) = store(); + + let error = store + .create(CreateInferenceProvider { + name: "OpenAI".to_string(), + format: InferenceProviderFormat::OpenAiResponses, + api_base_url: " ".to_string(), + api_key: "secret".to_string(), + }) + .unwrap_err(); + + assert!(matches!(error, InferenceProviderError::EmptyApiBaseUrl)); + } } From 1b649bf58795ff1dddcd1ff22125e2d54796c481 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sat, 25 Apr 2026 17:19:16 +0800 Subject: [PATCH 06/62] fix(desktop): simplify provider icon --- crates/desktop/src/pages/inference-providers.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index 21152181..67662256 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -3,7 +3,6 @@ import { ClipboardDocumentIcon, EyeIcon, EyeSlashIcon, - KeyIcon, PencilIcon, PlusIcon, TrashIcon, @@ -103,6 +102,12 @@ function ProviderIcon({ format: InferenceProviderFormatDto; isActive?: boolean; }) { + const marker = { + anthropic: "A", + openai_completions: "C", + openai_responses: "R", + }[format]; + return (
- + {format}
); From 2fd7b962210a4f99b0d7b0f4889b0b39d0af39da Mon Sep 17 00:00:00 2001 From: akarachen Date: Sat, 25 Apr 2026 17:26:34 +0800 Subject: [PATCH 07/62] fix(desktop): align provider detail layout --- .../desktop/src/pages/inference-providers.tsx | 291 +++++++++--------- 1 file changed, 147 insertions(+), 144 deletions(-) diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index 67662256..bff784e0 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -3,6 +3,7 @@ import { ClipboardDocumentIcon, EyeIcon, EyeSlashIcon, + GlobeAltIcon, PencilIcon, PlusIcon, TrashIcon, @@ -95,33 +96,15 @@ function formatDescription( return option ? t(option.descriptionKey) : format; } -function ProviderIcon({ - format, - isActive = false, -}: { - format: InferenceProviderFormatDto; - isActive?: boolean; -}) { - const marker = { - anthropic: "A", - openai_completions: "C", - openai_responses: "R", - }[format]; - +function ProviderIcon({ isActive = false }: { isActive?: boolean }) { return (
- - {format} +
); } @@ -548,129 +531,150 @@ function ProviderDetail({ }; return ( -
-
-
- -
-

- {provider.name} -

-

- {formatOption(provider.format, t)} -

-
-
-
- - - - - {t("edit")} - - - - - - {t("delete")} - + <> +
+
+ + +
+ +
+

+ {provider.name} +

+

+ {formatOption(provider.format, t)} +

+
+
+
+ + + + + + {t("edit")} + + + + + + + + {t("delete")} + + +
+
+ + +
+
+

+ {t("providerFormat")} +

+ + {formatOption(provider.format, t)} + +
+

+ {formatDescription(provider.format, t)} +

+
+ +
+
+

+ {t("providerApiBaseUrl")} +

+

+ {t("providerApiBaseUrlDescription")} +

+
+
+ + {provider.api_base_url} + +
+
+ +
+
+

+ {t("providerApiKey")} +

+

+ {t("providerApiKeyStored")} +

+
+
+ + {revealedKey ?? + "••••••••••••••••••••••••"} + + + +
+
+
+
-
- - -
- {t("providerFormat")} - - {formatDescription(provider.format, t)} - -
- - {formatOption(provider.format, t)} - -
-
- - - -
- {t("providerApiBaseUrl")} - - {t("providerApiBaseUrlDescription")} - -
-
- - - {provider.api_base_url} - - -
- - - -
- {t("providerApiKey")} - - {t("providerApiKeyStored")} - -
-
- -
- - {revealedKey ?? "••••••••••••••••••••••••"} - - - -
-
-
-
- -
+ ); } @@ -849,7 +853,6 @@ export default function InferenceProvidersPage() { >
Date: Sat, 25 Apr 2026 17:37:28 +0800 Subject: [PATCH 08/62] fix(desktop): simplify provider details --- crates/desktop/src/lib/locales/en.ts | 2 - crates/desktop/src/lib/locales/zh-Hans.ts | 2 - crates/desktop/src/lib/locales/zh-Hant.ts | 2 - .../desktop/src/pages/inference-providers.tsx | 57 ++++--------------- 4 files changed, 10 insertions(+), 53 deletions(-) diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index 6f858792..08d17744 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -110,11 +110,9 @@ export default { providerFormat: "Format", providerApiBaseUrl: "API Base URL", providerApiBaseUrlPlaceholder: "https://api.openai.com/v1", - providerApiBaseUrlDescription: "Root endpoint used for provider requests.", providerApiKey: "API Key", providerApiKeyPlaceholder: "sk-...", providerApiKeyEditPlaceholder: "Leave empty to keep current key", - providerApiKeyStored: "Stored in the native credential store.", providerApiKeyCopied: "API key copied", providerApiKeyCopyFailed: "Failed to copy API key", copyProviderApiKey: "Copy API key", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index 70b9bf1e..f582caaf 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -106,11 +106,9 @@ export default { providerFormat: "格式", providerApiBaseUrl: "API Base URL", providerApiBaseUrlPlaceholder: "https://api.openai.com/v1", - providerApiBaseUrlDescription: "用于发起 Provider 请求的根端点。", providerApiKey: "API Key", providerApiKeyPlaceholder: "sk-...", providerApiKeyEditPlaceholder: "留空以保持当前 key", - providerApiKeyStored: "存储在系统原生凭据库中。", providerApiKeyCopied: "API key 已复制", providerApiKeyCopyFailed: "复制 API key 失败", copyProviderApiKey: "复制 API key", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index b1c86a7b..720aca01 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -106,11 +106,9 @@ export default { providerFormat: "格式", providerApiBaseUrl: "API Base URL", providerApiBaseUrlPlaceholder: "https://api.openai.com/v1", - providerApiBaseUrlDescription: "用於發起 Provider 請求的根端點。", providerApiKey: "API Key", providerApiKeyPlaceholder: "sk-...", providerApiKeyEditPlaceholder: "留空以保持目前 key", - providerApiKeyStored: "儲存在系統原生憑證庫中。", providerApiKeyCopied: "API key 已複製", providerApiKeyCopyFailed: "複製 API key 失敗", copyProviderApiKey: "複製 API key", diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index bff784e0..d6a9d654 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -88,22 +88,9 @@ function formatOption( return option ? t(option.labelKey) : format; } -function formatDescription( - format: InferenceProviderFormatDto, - t: (key: string) => string, -) { - const option = FORMAT_OPTIONS.find((item) => item.id === format); - return option ? t(option.descriptionKey) : format; -} - -function ProviderIcon({ isActive = false }: { isActive?: boolean }) { +function ProviderIcon() { return ( -
+
); @@ -537,7 +524,7 @@ function ProviderDetail({
- +

{provider.name} @@ -605,20 +592,12 @@ function ProviderDetail({ {formatOption(provider.format, t)}

-

- {formatDescription(provider.format, t)} -

-
-

- {t("providerApiBaseUrl")} -

-

- {t("providerApiBaseUrlDescription")} -

-
+

+ {t("providerApiBaseUrl")} +

{provider.api_base_url} @@ -627,14 +606,9 @@ function ProviderDetail({
-
-

- {t("providerApiKey")} -

-

- {t("providerApiKeyStored")} -

-
+

+ {t("providerApiKey")} +

{revealedKey ?? @@ -852,22 +826,11 @@ export default function InferenceProvidersPage() { className="data-selected:bg-surface" >
- +
- - {formatOption( - provider.format, - t, - )} -
From 62afcb86185b45c13217556138f8d8b4ae4c8f29 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sat, 25 Apr 2026 17:52:43 +0800 Subject: [PATCH 09/62] fix(desktop): use branded provider icons --- crates/desktop/bun.lock | 999 ++++++++++++++++++ crates/desktop/package.json | 1 + .../desktop/src/pages/inference-providers.tsx | 69 +- 3 files changed, 1037 insertions(+), 32 deletions(-) diff --git a/crates/desktop/bun.lock b/crates/desktop/bun.lock index 82575de7..e3adacff 100644 --- a/crates/desktop/bun.lock +++ b/crates/desktop/bun.lock @@ -8,6 +8,7 @@ "@heroicons/react": "^2.2.0", "@heroui/react": "^3.0.1", "@heroui/styles": "^3.0.1", + "@lobehub/icons": "^5.5.4", "@tanstack/react-query": "^5.94.5", "@tauri-apps/api": "^2", "@tauri-apps/plugin-deep-link": "^2.4.7", @@ -64,6 +65,20 @@ }, }, "packages": { + "@ant-design/colors": ["@ant-design/colors@8.0.1", "", { "dependencies": { "@ant-design/fast-color": "^3.0.0" } }, "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ=="], + + "@ant-design/cssinjs": ["@ant-design/cssinjs@2.1.2", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ=="], + + "@ant-design/cssinjs-utils": ["@ant-design/cssinjs-utils@2.1.2", "", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@babel/runtime": "^7.23.2", "@rc-component/util": "^1.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA=="], + + "@ant-design/fast-color": ["@ant-design/fast-color@3.0.1", "", {}, "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw=="], + + "@ant-design/icons": ["@ant-design/icons@6.1.1", "", { "dependencies": { "@ant-design/colors": "^8.0.0", "@ant-design/icons-svg": "^4.4.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-AMT4N2y++TZETNHiM77fs4a0uPVCJGuL5MTonk13Pvv7UN7sID1cNEZOc1qNqx6zLKAOilTEFAdAoAFKa0U//Q=="], + + "@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="], + + "@ant-design/react-slick": ["@ant-design/react-slick@2.0.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "clsx": "^2.1.1", "json2mq": "^0.2.0", "throttle-debounce": "^5.0.0" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg=="], + "@antfu/eslint-config": ["@antfu/eslint-config@8.1.1", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@clack/prompts": "^1.2.0", "@e18e/eslint-plugin": "^0.3.0", "@eslint-community/eslint-plugin-eslint-comments": "^4.7.1", "@eslint/markdown": "^8.0.1", "@stylistic/eslint-plugin": "^5.10.0", "@typescript-eslint/eslint-plugin": "^8.58.1", "@typescript-eslint/parser": "^8.58.1", "@vitest/eslint-plugin": "^1.6.14", "ansis": "^4.2.0", "cac": "^7.0.0", "eslint-config-flat-gitignore": "^2.3.0", "eslint-flat-config-utils": "^3.1.0", "eslint-merge-processors": "^2.0.0", "eslint-plugin-antfu": "^3.2.2", "eslint-plugin-command": "^3.5.2", "eslint-plugin-import-lite": "^0.6.0", "eslint-plugin-jsdoc": "^62.9.0", "eslint-plugin-jsonc": "^3.1.2", "eslint-plugin-n": "^17.24.0", "eslint-plugin-no-only-tests": "^3.3.0", "eslint-plugin-perfectionist": "^5.8.0", "eslint-plugin-pnpm": "^1.6.0", "eslint-plugin-regexp": "^3.1.0", "eslint-plugin-toml": "^1.3.1", "eslint-plugin-unicorn": "^64.0.0", "eslint-plugin-unused-imports": "^4.4.1", "eslint-plugin-vue": "^10.8.0", "eslint-plugin-yml": "^3.3.1", "eslint-processor-vue-blocks": "^2.0.0", "globals": "^17.4.0", "local-pkg": "^1.1.2", "parse-gitignore": "^2.0.0", "toml-eslint-parser": "^1.0.3", "vue-eslint-parser": "^10.4.0", "yaml-eslint-parser": "^2.0.0" }, "peerDependencies": { "@angular-eslint/eslint-plugin": "^21.1.0", "@angular-eslint/eslint-plugin-template": "^21.1.0", "@angular-eslint/template-parser": "^21.1.0", "@eslint-react/eslint-plugin": "^3.0.0", "@next/eslint-plugin-next": ">=15.0.0", "@prettier/plugin-xml": "^3.4.1", "@unocss/eslint-plugin": ">=0.50.0", "astro-eslint-parser": "^1.0.2", "eslint": "^9.10.0 || ^10.0.0", "eslint-plugin-astro": "^1.2.0", "eslint-plugin-format": ">=0.1.0", "eslint-plugin-jsx-a11y": ">=6.10.2", "eslint-plugin-react-refresh": "^0.5.0", "eslint-plugin-solid": "^0.14.3", "eslint-plugin-svelte": ">=2.35.1", "eslint-plugin-vuejs-accessibility": "^2.4.1", "prettier-plugin-astro": "^0.14.0", "prettier-plugin-slidev": "^1.0.5", "svelte-eslint-parser": ">=0.37.0" }, "optionalPeers": ["@angular-eslint/eslint-plugin", "@angular-eslint/eslint-plugin-template", "@angular-eslint/template-parser", "@eslint-react/eslint-plugin", "@next/eslint-plugin-next", "@prettier/plugin-xml", "@unocss/eslint-plugin", "astro-eslint-parser", "eslint-plugin-astro", "eslint-plugin-format", "eslint-plugin-jsx-a11y", "eslint-plugin-react-refresh", "eslint-plugin-solid", "eslint-plugin-svelte", "eslint-plugin-vuejs-accessibility", "prettier-plugin-astro", "prettier-plugin-slidev", "svelte-eslint-parser"], "bin": { "eslint-config": "bin/index.mjs" } }, "sha512-y5/eAKlJUbQpeES2Pnb0i/VgbmqQ+srHJJNqbTKEBsxdLy3h1BqdS00zDpE+YeP71EWmlYJSTUhcJg4n4yMeAQ=="], "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], @@ -104,10 +119,36 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@base-ui/react": ["@base-ui/react@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@base-ui/utils": "0.2.3", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "tabbable": "^6.3.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-4USBWz++DUSLTuIYpbYkSgy1F9ZmNG9S/lXvlUN6qMK0P0RlW+6eQmDUB4DgZ7HVvtXl4pvi4z5J2fv6Z3+9hg=="], + + "@base-ui/utils": ["@base-ui/utils@0.2.3", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-/CguQ2PDaOzeVOkllQR8nocJ0FFIDqsWIcURsVmm53QGo8NhFNpePjNlyPIB41luxfOqnG7PU0xicMEw3ls7XQ=="], + + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], + + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="], + + "@chevrotain/gast": ["@chevrotain/gast@12.0.0", "", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="], + + "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@12.0.0", "", {}, "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA=="], + + "@chevrotain/types": ["@chevrotain/types@12.0.0", "", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="], + + "@chevrotain/utils": ["@chevrotain/utils@12.0.0", "", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="], + "@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="], "@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="], + "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], + + "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], + + "@dnd-kit/modifiers": ["@dnd-kit/modifiers@9.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw=="], + + "@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="], + + "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + "@e18e/eslint-plugin": ["@e18e/eslint-plugin@0.3.0", "", { "dependencies": { "eslint-plugin-depend": "^1.5.0" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0", "oxlint": "^1.55.0" }, "optionalPeers": ["eslint", "oxlint"] }, "sha512-hHgfpxsrZ2UYHcicA+tGZnmk19uJTaye9VH79O+XS8R4ona2Hx3xjhXghclNW58uXMk3xXlbYEOMr8thsoBmWg=="], "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], @@ -116,6 +157,36 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@emoji-mart/data": ["@emoji-mart/data@1.2.1", "", {}, "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw=="], + + "@emoji-mart/react": ["@emoji-mart/react@1.1.1", "", { "peerDependencies": { "emoji-mart": "^5.2", "react": "^16.8 || ^17 || ^18" } }, "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g=="], + + "@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="], + + "@emotion/cache": ["@emotion/cache@11.14.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="], + + "@emotion/css": ["@emotion/css@11.13.5", "", { "dependencies": { "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.13.5", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2" } }, "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w=="], + + "@emotion/hash": ["@emotion/hash@0.8.0", "", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="], + + "@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.4.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="], + + "@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], + + "@emotion/react": ["@emotion/react@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="], + + "@emotion/serialize": ["@emotion/serialize@1.3.3", "", { "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA=="], + + "@emotion/sheet": ["@emotion/sheet@1.4.0", "", {}, "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="], + + "@emotion/unitless": ["@emotion/unitless@0.7.5", "", {}, "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="], + + "@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.2.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg=="], + + "@emotion/utils": ["@emotion/utils@1.4.2", "", {}, "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="], + + "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="], + "@es-joy/jsdoccomment": ["@es-joy/jsdoccomment@0.84.0", "", { "dependencies": { "@types/estree": "^1.0.8", "@typescript-eslint/types": "^8.54.0", "comment-parser": "1.4.5", "esquery": "^1.7.0", "jsdoc-type-pratt-parser": "~7.1.1" } }, "sha512-0xew1CxOam0gV5OMjh2KjFQZsKL2bByX1+q4j3E73MpYIdyUxcZb/xQct9ccUb+ve5KGUYbCUxyPnYB7RbuP+w=="], "@es-joy/resolve.exports": ["@es-joy/resolve.exports@1.2.0", "", {}, "sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g=="], @@ -154,6 +225,16 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/react": ["@floating-ui/react@0.27.19", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@formatjs/ecma402-abstract": ["@formatjs/ecma402-abstract@2.3.6", "", { "dependencies": { "@formatjs/fast-memoize": "2.2.7", "@formatjs/intl-localematcher": "0.6.2", "decimal.js": "^10.4.3", "tslib": "^2.8.0" } }, "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw=="], "@formatjs/fast-memoize": ["@formatjs/fast-memoize@2.2.7", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ=="], @@ -164,6 +245,8 @@ "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="], + "@giscus/react": ["@giscus/react@3.1.0", "", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="], + "@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="], "@heroui/react": ["@heroui/react@3.0.2", "", { "dependencies": { "@heroui/styles": "3.0.2", "@radix-ui/react-avatar": "1.1.11", "@react-aria/i18n": "3.12.16", "@react-aria/ssr": "3.9.10", "@react-aria/utils": "3.33.1", "@react-stately/utils": "3.11.0", "@react-types/color": "3.1.4", "@react-types/shared": "3.33.1", "input-otp": "1.4.2", "react-aria-components": "1.16.0", "tailwind-merge": "3.4.0", "tailwind-variants": "3.2.2" }, "peerDependencies": { "react": ">=19.0.0", "react-dom": ">=19.0.0", "tailwindcss": ">=4.0.0" } }, "sha512-HWcYFurH+OnLITgIvQKyCd6BhYLApyzg0qqL3T5xemK5hgo1Nr+wQGQ5JSNVfBAmF4tWSS9TOzr24UHEO+21Ww=="], @@ -178,6 +261,10 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], + "@internationalized/date": ["@internationalized/date@3.12.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ=="], "@internationalized/message": ["@internationalized/message@3.1.8", "", { "dependencies": { "@swc/helpers": "^0.5.0", "intl-messageformat": "^10.1.0" } }, "sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA=="], @@ -198,6 +285,24 @@ "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], + "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.5.1", "", {}, "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA=="], + + "@lit/reactive-element": ["@lit/reactive-element@2.1.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="], + + "@lobehub/emojilib": ["@lobehub/emojilib@1.0.0", "", {}, "sha512-s9KnjaPjsEefaNv150G3aifvB+J3P4eEKG+epY9zDPS2BeB6+V2jELWqAZll+nkogMaVovjEE813z3V751QwGw=="], + + "@lobehub/fluent-emoji": ["@lobehub/fluent-emoji@4.1.0", "", { "dependencies": { "@lobehub/emojilib": "^1.0.0", "antd-style": "^4.1.0", "emoji-regex": "^10.6.0", "es-toolkit": "^1.43.0", "lucide-react": "^0.562.0", "url-join": "^5.0.0" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-R1MB2lfUkDvB7XAQdRzY75c1dx/tB7gEvBPaEEMarzKfCJWmXm7rheS6caVzmgwAlq5sfmTbxPL+un99sp//Yw=="], + + "@lobehub/icons": ["@lobehub/icons@5.5.4", "", { "dependencies": { "antd-style": "^4.1.0", "es-toolkit": "^1.45.1", "lucide-react": "^0.469.0", "polished": "^4.3.1" }, "peerDependencies": { "@lobehub/ui": "^5.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-xxXGx/vQg1iXK6C/a2jlORCZycF7r46U5047kAdmYuvlczTC8PJ4WLPTjLk/0kG6DWjwFKHfCyP4ItM5dPOFbQ=="], + + "@lobehub/ui": ["@lobehub/ui@5.9.6", "", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@base-ui/react": "1.0.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@emotion/is-prop-valid": "^1.4.0", "@floating-ui/react": "^0.27.19", "@giscus/react": "^3.1.0", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@pierre/diffs": "^1.1.10", "@radix-ui/react-slot": "^1.2.4", "@shikijs/core": "^4.0.2", "@shikijs/transformers": "^4.0.2", "@splinetool/runtime": "0.9.526", "ahooks": "^3.9.7", "antd-style": "^4.1.0", "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.20", "emoji-mart": "^5.6.0", "es-toolkit": "^1.45.1", "fast-deep-equal": "^3.1.3", "immer": "^11.1.4", "katex": "^0.16.44", "leva": "^0.10.1", "lucide-react": "^1.7.0", "marked": "^17.0.5", "mermaid": "^11.14.0", "motion": "^12.38.0", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.3.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.12.0", "rc-input-number": "^9.5.0", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^15.1.0", "react-error-boundary": "^6.1.1", "react-hotkeys-hook": "^5.2.4", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.3", "react-zoom-pan-pinch": "^3.7.0", "rehype-github-alerts": "^4.2.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-cjk-friendly": "^2.0.1", "remark-gfm": "^4.0.1", "remark-github": "^12.0.0", "remark-math": "^6.0.0", "remend": "^1.3.0", "shiki": "^4.0.2", "shiki-stream": "^0.1.4", "swr": "^2.4.1", "ts-md5": "^2.0.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^13.0.0", "virtua": "^0.49.0" }, "peerDependencies": { "@lobehub/fluent-emoji": "^4.0.0", "@lobehub/icons": "^5.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-QHBAiJWZvSuFcLrkbigEDYGSNsXLvXattCASoZBVfoXMFWCbGgE1+5oIaboHjuSeKiemdss0OgIMnasa9y8EqQ=="], + + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + + "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], + + "@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -290,24 +395,146 @@ "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.19.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw=="], + "@pierre/diffs": ["@pierre/diffs@1.1.19", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-eYyDW69heXd7i9zdkWogGYosHzoYF2dstV6uDcmnQAf72uRChs3hrpf/7ym/ayTiwD8a+TQ7oZ5vNNb0tstJvA=="], + + "@pierre/theme": ["@pierre/theme@0.0.28", "", {}, "sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw=="], + "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], + "@primer/octicons": ["@primer/octicons@19.25.0", "", { "dependencies": { "object-assign": "^4.1.1" } }, "sha512-E0eMV8nXexrs7Vro7PdS8v/JfvvYCMh8HN6CXJ9l8fk9atZaY05fVUcyiAh5KjEJu7IxdFy4URfHGpM7+iOl1A=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], "@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4kY9IVa6+9nJPsYmngK5Uk2kUmZnv7ChhHAFeQ5oaj8jrR1bIi3xww8nH71pz1/Ve4d/cXO3YxT8eikt1B0a8w=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="], "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + + "@rc-component/async-validator": ["@rc-component/async-validator@5.1.0", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA=="], + + "@rc-component/cascader": ["@rc-component/cascader@1.14.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ=="], + + "@rc-component/checkbox": ["@rc-component/checkbox@2.0.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ=="], + + "@rc-component/collapse": ["@rc-component/collapse@1.2.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw=="], + + "@rc-component/color-picker": ["@rc-component/color-picker@3.1.1", "", { "dependencies": { "@ant-design/fast-color": "^3.0.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg=="], + + "@rc-component/context": ["@rc-component/context@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw=="], + + "@rc-component/dialog": ["@rc-component/dialog@1.8.4", "", { "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw=="], + + "@rc-component/drawer": ["@rc-component/drawer@1.4.2", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.1.3", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q=="], + + "@rc-component/dropdown": ["@rc-component/dropdown@1.0.2", "", { "dependencies": { "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg=="], + + "@rc-component/form": ["@rc-component/form@1.8.1", "", { "dependencies": { "@rc-component/async-validator": "^5.1.0", "@rc-component/util": "^1.6.2", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-8O7TB55Fi2mWIGvSnwZjk8jFqVNYyKDAswglwGShcbndxqzKz4cHwNtNaLjZlAeRge9wcB0LL8IWsC/Bl18raQ=="], + + "@rc-component/image": ["@rc-component/image@1.9.0", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ=="], + + "@rc-component/input": ["@rc-component/input@1.1.2", "", { "dependencies": { "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg=="], + + "@rc-component/input-number": ["@rc-component/input-number@1.6.2", "", { "dependencies": { "@rc-component/mini-decimal": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w=="], + + "@rc-component/mentions": ["@rc-component/mentions@1.6.0", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/menu": "~1.2.0", "@rc-component/textarea": "~1.1.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ=="], + + "@rc-component/menu": ["@rc-component/menu@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg=="], + + "@rc-component/mini-decimal": ["@rc-component/mini-decimal@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.18.0" } }, "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw=="], + + "@rc-component/motion": ["@rc-component/motion@1.3.2", "", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-itfd+GztzJYAb04Z4RkEub1TbJAfZc2Iuy8p44U44xD1F5+fNYFKI3897ijlbIyfvXkTmMm+KGcjkQQGMHywEQ=="], + + "@rc-component/mutate-observer": ["@rc-component/mutate-observer@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w=="], + + "@rc-component/notification": ["@rc-component/notification@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA=="], + + "@rc-component/overflow": ["@rc-component/overflow@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-syfmgAABaHCnCDzPwHZ/2tuvIcpOO3jefYZMmfkN+pmo8HKTzsfhS57vxo4ksPdN0By+uWVJhJWNFozNBxi2eA=="], + + "@rc-component/pagination": ["@rc-component/pagination@1.2.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw=="], + + "@rc-component/picker": ["@rc-component/picker@1.9.1", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g=="], + + "@rc-component/portal": ["@rc-component/portal@1.1.2", "", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg=="], + + "@rc-component/progress": ["@rc-component/progress@1.0.2", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ=="], + + "@rc-component/qrcode": ["@rc-component/qrcode@1.1.1", "", { "dependencies": { "@babel/runtime": "^7.24.7" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA=="], + + "@rc-component/rate": ["@rc-component/rate@1.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw=="], + + "@rc-component/resize-observer": ["@rc-component/resize-observer@1.1.2", "", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q=="], + + "@rc-component/segmented": ["@rc-component/segmented@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg=="], + + "@rc-component/select": ["@rc-component/select@1.6.15", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-SyVCWnqxCQZZcQvQJ/CxSjx2bGma6ds/HtnpkIfZVnt6RoEgbqUmHgD6vrzNarNXwbLXerwVzWwq8F3d1sst7g=="], + + "@rc-component/slider": ["@rc-component/slider@1.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g=="], + + "@rc-component/steps": ["@rc-component/steps@1.2.2", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw=="], + + "@rc-component/switch": ["@rc-component/switch@1.0.3", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw=="], + + "@rc-component/table": ["@rc-component/table@1.9.1", "", { "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.1.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg=="], + + "@rc-component/tabs": ["@rc-component/tabs@1.7.0", "", { "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w=="], + + "@rc-component/textarea": ["@rc-component/textarea@1.1.2", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A=="], + + "@rc-component/tooltip": ["@rc-component/tooltip@1.4.0", "", { "dependencies": { "@rc-component/trigger": "^3.7.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg=="], + + "@rc-component/tour": ["@rc-component/tour@2.3.0", "", { "dependencies": { "@rc-component/portal": "^2.2.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow=="], + + "@rc-component/tree": ["@rc-component/tree@1.2.4", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.8.1", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w=="], + + "@rc-component/tree-select": ["@rc-component/tree-select@1.8.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ=="], + + "@rc-component/trigger": ["@rc-component/trigger@3.9.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.2.0", "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg=="], + + "@rc-component/upload": ["@rc-component/upload@1.1.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw=="], + + "@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/virtual-list": ["@rc-component/virtual-list@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ=="], + "@react-aria/autocomplete": ["@react-aria/autocomplete@3.0.0-rc.6", "", { "dependencies": { "@react-aria/combobox": "^3.15.0", "@react-aria/focus": "^3.21.5", "@react-aria/i18n": "^3.12.16", "@react-aria/interactions": "^3.27.1", "@react-aria/listbox": "^3.15.3", "@react-aria/searchfield": "^3.8.12", "@react-aria/textfield": "^3.18.5", "@react-aria/utils": "^3.33.1", "@react-stately/autocomplete": "3.0.0-beta.4", "@react-stately/combobox": "^3.13.0", "@react-types/autocomplete": "3.0.0-alpha.38", "@react-types/button": "^3.15.1", "@react-types/shared": "^3.33.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-uymUNJ8NW+dX7lmgkHE+SklAbxwktycAJcI5lBBw6KPZyc0EdMHC+/Fc5CUz3enIAhNwd2oxxogcSHknquMzQA=="], "@react-aria/breadcrumbs": ["@react-aria/breadcrumbs@3.5.32", "", { "dependencies": { "@react-aria/i18n": "^3.12.16", "@react-aria/link": "^3.8.9", "@react-aria/utils": "^3.33.1", "@react-types/breadcrumbs": "^3.7.19", "@react-types/shared": "^3.33.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-S61vh5DJ2PXiXUwD7gk+pvS/b4VPrc3ZJOUZ0yVRLHkVESr5LhIZH+SAVgZkm1lzKyMRG+BH+fiRH/DZRSs7SA=="], @@ -558,10 +785,32 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], + "@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg=="], + + "@shikijs/langs": ["@shikijs/langs@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg=="], + + "@shikijs/primitive": ["@shikijs/primitive@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw=="], + + "@shikijs/themes": ["@shikijs/themes@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="], + + "@shikijs/transformers": ["@shikijs/transformers@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/types": "4.0.2" } }, "sha512-1+L0gf9v+SdDXs08vjaLb3mBFa8U7u37cwcBQIv/HCocLwX69Tt6LpUCjtB+UUTvQxI7BnjZKhN/wMjhHBcJGg=="], + + "@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@sindresorhus/base62": ["@sindresorhus/base62@1.0.0", "", {}, "sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA=="], + "@splinetool/runtime": ["@splinetool/runtime@0.9.526", "", { "dependencies": { "on-change": "^4.0.0", "semver-compare": "^1.0.0" } }, "sha512-qznHbXA5aKwDbCgESAothCNm1IeEZcmNWG145p5aXj4w5uoqR1TZ9qkTHTKLTsUbHeitCwdhzmRqan1kxboLgQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@stitches/react": ["@stitches/react@1.2.8", "", { "peerDependencies": { "react": ">= 16.3.0" } }, "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA=="], + "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.10.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/types": "^8.56.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0" } }, "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ=="], "@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], @@ -642,28 +891,102 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + + "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + + "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + + "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + + "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], + + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + + "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/js-cookie": ["@types/js-cookie@3.0.6", "", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/type-utils": "8.58.1", "@typescript-eslint/utils": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ=="], @@ -688,6 +1011,14 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], + + "@use-gesture/core": ["@use-gesture/core@10.3.1", "", {}, "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw=="], + + "@use-gesture/react": ["@use-gesture/react@10.3.1", "", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="], + "@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], @@ -708,6 +1039,8 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "ahooks": ["ahooks@3.9.7", "", { "dependencies": { "@babel/runtime": "^7.21.0", "@types/js-cookie": "^3.0.6", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-S0lvzhbdlhK36RFBkGv+RbOM/dbbweym+BIHM/bwwuWVSVN5TuVErHPMWo4w0t1NDYg5KPp2iEf7Y7E5LASYiw=="], + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -716,10 +1049,24 @@ "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], + "antd": ["antd@6.3.6", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", "@ant-design/icons": "^6.1.1", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.28.4", "@rc-component/cascader": "~1.14.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.8.4", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.8.0", "@rc-component/image": "~1.9.0", "@rc-component/input": "~1.1.2", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.6.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.3.2", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~1.2.0", "@rc-component/pagination": "~1.2.0", "@rc-component/picker": "~1.9.1", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.9.1", "@rc-component/tabs": "~1.7.0", "@rc-component/textarea": "~1.1.2", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.3.0", "@rc-component/tree": "~1.2.4", "@rc-component/tree-select": "~1.8.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-zdCYjusrTUn4gNxEg4PH8MWlfuXYbKfuGOkjgZ0Rg6DpWbIVmG/MwvsZ5yvG6z3Y6UI/gzYpaQ82iTt4KdbeaA=="], + + "antd-style": ["antd-style@4.1.0", "", { "dependencies": { "@ant-design/cssinjs": "^2.0.0", "@babel/runtime": "^7.24.1", "@emotion/cache": "^11.11.0", "@emotion/css": "^11.11.2", "@emotion/react": "^11.11.4", "@emotion/serialize": "^1.1.3", "@emotion/utils": "^1.2.1", "use-merge-value": "^1.2.0" }, "peerDependencies": { "antd": ">=6.0.0", "react": ">=18" } }, "sha512-vnPBGg0OVlSz90KRYZhxd89aZiOImTiesF+9MQqN8jsLGZUQTjbP04X9jTdEfsztKUuMbBWg/RmB/wHTakbtMQ=="], + "are-docs-informative": ["are-docs-informative@0.0.2", "", {}, "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig=="], + "assign-symbols": ["assign-symbols@1.0.0", "", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="], + + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + + "attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="], + + "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="], + "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.17", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA=="], @@ -740,6 +1087,8 @@ "cac": ["cac@7.0.0", "", {}, "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001787", "", {}, "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -750,8 +1099,24 @@ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="], + + "chevrotain-allstar": ["chevrotain-allstar@0.4.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^12.0.0" } }, "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA=="], + + "chroma-js": ["chroma-js@3.2.0", "", {}, "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw=="], + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], + "clean-regexp": ["clean-regexp@1.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw=="], "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], @@ -762,50 +1127,146 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "comment-parser": ["comment-parser@1.4.6", "", {}, "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg=="], "compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="], + "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], + "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "core-js-compat": ["core-js-compat@3.49.0", "", { "dependencies": { "browserslist": "^4.28.1" } }, "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA=="], + "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], + + "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "cytoscape": ["cytoscape@3.33.2", "", {}, "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw=="], + + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], + + "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], + + "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + + "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], + + "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + + "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], + + "dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "decode-uri-component": ["decode-uri-component@0.4.1", "", {}, "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], + "dompurify": ["dompurify@3.4.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw=="], + "dotenv": ["dotenv@17.4.1", "", {}, "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw=="], "driver.js": ["driver.js@1.4.0", "", {}, "sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew=="], "electron-to-chromium": ["electron-to-chromium@1.5.334", "", {}, "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog=="], + "emoji-mart": ["emoji-mart@5.6.0", "", {}, "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], @@ -814,6 +1275,16 @@ "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-toolkit": ["es-toolkit@1.46.0", "", {}, "sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA=="], + + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + + "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -896,12 +1367,28 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], + + "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -926,8 +1413,14 @@ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "file-selector": ["file-selector@0.5.0", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "filter-obj": ["filter-obj@5.1.0", "", {}, "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng=="], + + "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], "find-up-simple": ["find-up-simple@1.0.1", "", {}, "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ=="], @@ -936,12 +1429,18 @@ "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + "for-in": ["for-in@1.0.2", "", {}, "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ=="], + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="], + "framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "fuse.js": ["fuse.js@7.3.0", "", {}, "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], @@ -950,6 +1449,10 @@ "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], + "get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="], + + "giscus": ["giscus@1.6.0", "", { "dependencies": { "lit": "^3.2.1" } }, "sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ=="], + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -960,48 +1463,120 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + + "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], + + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], + + "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="], + + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + + "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], + + "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + "i18next": ["i18next@26.0.4", "", { "dependencies": { "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-gXF7U9bfioXPLv7mw8Qt2nfO7vij5MyINvPgVv99pX3fL1Y01pw2mKBFrlYpRxRCl2wz3ISenj6VsMJT2isfuA=="], "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + "input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="], + "internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + + "intersection-observer": ["intersection-observer@0.12.2", "", {}, "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="], + "intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="], + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + "is-builtin-module": ["is-builtin-module@5.0.0", "", { "dependencies": { "builtin-modules": "^5.0.0" } }, "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-extendable": ["is-extendable@1.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4" } }, "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + "is-mobile": ["is-mobile@5.0.0", "", {}, "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], @@ -1012,10 +1587,14 @@ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json2mq": ["json2mq@0.2.0", "", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "jsonc-eslint-parser": ["jsonc-eslint-parser@3.1.0", "", { "dependencies": { "acorn": "^8.5.0", "eslint-visitor-keys": "^5.0.0", "semver": "^7.3.5" } }, "sha512-75EA7EWZExL/j+MDKQrRbdzcRI2HOkRlmUw8fZJc1ioqFEOvBsq7Rt+A6yCxOt9w/TYNpkt52gC6nm/g5tFIng=="], @@ -1026,12 +1605,20 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "knip": ["knip@6.3.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "get-tsconfig": "4.13.7", "jiti": "^2.6.0", "minimist": "^1.2.8", "oxc-parser": "^0.121.0", "oxc-resolver": "^11.19.1", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.6.1", "strip-json-comments": "5.0.3", "unbash": "^2.2.0", "yaml": "^2.8.2", "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-22kLJloVcOVOAudCxlFOC0ICAMme7dKsS7pVTEnrmyKGpswb8ieznvAiSKUeFVDJhb01ect6dkDc1Ha1g1sPpg=="], "ky": ["ky@2.0.0", "", {}, "sha512-KzI4Vz5AbZFAUFYGx28PCSfFWUo6/qj9Br/P6KRwDieE1xfdz0tIONepJcLw/1xLocN13GgvfJGasa+pfSkbHg=="], + "langium": ["langium@4.2.2", "", { "dependencies": { "@chevrotain/regexp-to-ast": "~12.0.0", "chevrotain": "~12.0.0", "chevrotain-allstar": "~0.4.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ=="], + + "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + + "leva": ["leva@0.10.1", "", { "dependencies": { "@radix-ui/react-portal": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.8", "@stitches/react": "^1.2.8", "@use-gesture/react": "^10.2.5", "colord": "^2.9.2", "dequal": "^2.0.2", "merge-value": "^1.0.0", "react-colorful": "^5.5.1", "react-dropzone": "^12.0.0", "v8n": "^1.3.3", "zustand": "^3.6.9" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-BcjnfUX8jpmwZUz2L7AfBtF9vn4ggTH33hmeufDULbP3YgNZ/C+ss/oO3stbrqRQyaOmRwy70y7BGTGO81S3rA=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -1058,10 +1645,22 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "lit": ["lit@3.3.2", "", { "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", "lit-html": "^3.3.0" } }, "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ=="], + + "lit-element": ["lit-element@4.2.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0", "@lit/reactive-element": "^2.1.0", "lit-html": "^3.3.0" } }, "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w=="], + + "lit-html": ["lit-html@3.3.2", "", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw=="], + "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], + "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], @@ -1070,12 +1669,22 @@ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], + + "lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], @@ -1096,20 +1705,40 @@ "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-newline-to-break": ["mdast-util-newline-to-break@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0" } }, "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog=="], + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + "merge-value": ["merge-value@1.0.0", "", { "dependencies": { "get-value": "^2.0.6", "is-extendable": "^1.0.0", "mixin-deep": "^1.2.0", "set-value": "^2.0.0" } }, "sha512-fJMmvat4NeKz63Uv9iHWcPDjCWcCkoiRoajRTEO8hlhUC6rwaHg0QCF9hBOTjZmm4JuglPckPSTtcuJL5kp0TQ=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "mermaid": ["mermaid@11.14.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@2.0.1", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw=="], + + "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@3.0.1", "", { "dependencies": { "get-east-asian-width": "^1.4.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w=="], + "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], @@ -1128,10 +1757,22 @@ "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], @@ -1152,6 +1793,8 @@ "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], @@ -1176,10 +1819,18 @@ "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + "mixin-deep": ["mixin-deep@1.3.2", "", { "dependencies": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" } }, "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA=="], + "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], "module-replacements": ["module-replacements@2.11.0", "", {}, "sha512-j5sNQm3VCpQQ7nTqGeOZtoJtV3uKERgCBm9QRhmGRiXiqkf7iRFOkfxdJRZWLkqYY8PNf4cDQF/WfXUYLENrRA=="], + "motion": ["motion@12.38.0", "", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="], + + "motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="], + + "motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nano-staged": ["nano-staged@1.0.2", "", { "bin": { "nano-staged": "lib/bin.js" } }, "sha512-Fytar3zHLY99nlMfqPPbraxZodqQAHPpdPRyYaplL+lB9DCR6pUrafxbG+Btz4+7fO5Rm/+DO4ZeDO/nLSUMhw=="], @@ -1194,12 +1845,22 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + "numeral": ["numeral@2.0.6", "", {}, "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA=="], + "nuqs": ["nuqs@2.8.9", "", { "dependencies": { "@standard-schema/spec": "1.0.0" }, "peerDependencies": { "@remix-run/react": ">=2", "@tanstack/react-router": "^1", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router": "^5 || ^6 || ^7", "react-router-dom": "^5 || ^6 || ^7" }, "optionalPeers": ["@remix-run/react", "@tanstack/react-router", "next", "react-router", "react-router-dom"] }, "sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "object-deep-merge": ["object-deep-merge@2.0.0", "", {}, "sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg=="], + "on-change": ["on-change@4.0.2", "", {}, "sha512-cMtCyuJmTx/bg2HCpHo3ZLeF7FZnBOapLqZHr2AlLeJ5Ul0Zu2mUJJz051Fdwu/Et2YW04ZD+TtU+gVy0ACNCA=="], + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], @@ -1214,16 +1875,30 @@ "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "parse-gitignore": ["parse-gitignore@2.0.0", "", {}, "sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog=="], "parse-imports-exports": ["parse-imports-exports@0.2.4", "", { "dependencies": { "parse-statements": "1.0.11" } }, "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ=="], + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + "parse-statements": ["parse-statements@1.0.11", "", {}, "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], @@ -1238,6 +1913,12 @@ "pnpm-workspace-yaml": ["pnpm-workspace-yaml@1.6.0", "", { "dependencies": { "yaml": "^2.8.2" } }, "sha512-uUy4dK3E11sp7nK+hnT7uAWfkBMe00KaUw8OG3NuNlYQoTk4sc9pcdIy1+XIP85v9Tvr02mK3JPaNNrP0QyRaw=="], + "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], + + "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], + + "polished": ["polished@4.3.1", "", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA=="], + "postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="], "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], @@ -1248,34 +1929,102 @@ "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + "query-string": ["query-string@9.3.1", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="], + "rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="], + + "rc-dialog": ["rc-dialog@9.6.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg=="], + + "rc-footer": ["rc-footer@0.6.8", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-JBZ+xcb6kkex8XnBd4VHw1ZxjV6kmcwUumSHaIFdka2qzMCo7Klcy4sI6G0XtUpG/vtpislQCc+S9Bc+NLHYMg=="], + + "rc-image": ["rc-image@7.12.0", "", { "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/portal": "^1.0.2", "classnames": "^2.2.6", "rc-dialog": "~9.6.0", "rc-motion": "^2.6.2", "rc-util": "^5.34.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q=="], + + "rc-input": ["rc-input@1.8.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.18.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA=="], + + "rc-input-number": ["rc-input-number@9.5.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/mini-decimal": "^1.0.1", "classnames": "^2.2.5", "rc-input": "~1.8.0", "rc-util": "^5.40.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag=="], + + "rc-menu": ["rc-menu@9.16.1", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^2.0.0", "classnames": "2.x", "rc-motion": "^2.4.3", "rc-overflow": "^1.3.1", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg=="], + + "rc-motion": ["rc-motion@2.9.5", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA=="], + + "rc-overflow": ["rc-overflow@1.5.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-resize-observer": "^1.0.0", "rc-util": "^5.37.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg=="], + + "rc-resize-observer": ["rc-resize-observer@1.4.3", "", { "dependencies": { "@babel/runtime": "^7.20.7", "classnames": "^2.2.1", "rc-util": "^5.44.1", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ=="], + + "rc-util": ["rc-util@5.44.4", "", { "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w=="], + + "re-resizable": ["re-resizable@6.11.2", "", { "peerDependencies": { "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A=="], + "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], "react-aria": ["react-aria@3.47.0", "", { "dependencies": { "@internationalized/string": "^3.2.7", "@react-aria/breadcrumbs": "^3.5.32", "@react-aria/button": "^3.14.5", "@react-aria/calendar": "^3.9.5", "@react-aria/checkbox": "^3.16.5", "@react-aria/color": "^3.1.5", "@react-aria/combobox": "^3.15.0", "@react-aria/datepicker": "^3.16.1", "@react-aria/dialog": "^3.5.34", "@react-aria/disclosure": "^3.1.3", "@react-aria/dnd": "^3.11.6", "@react-aria/focus": "^3.21.5", "@react-aria/gridlist": "^3.14.4", "@react-aria/i18n": "^3.12.16", "@react-aria/interactions": "^3.27.1", "@react-aria/label": "^3.7.25", "@react-aria/landmark": "^3.0.10", "@react-aria/link": "^3.8.9", "@react-aria/listbox": "^3.15.3", "@react-aria/menu": "^3.21.0", "@react-aria/meter": "^3.4.30", "@react-aria/numberfield": "^3.12.5", "@react-aria/overlays": "^3.31.2", "@react-aria/progress": "^3.4.30", "@react-aria/radio": "^3.12.5", "@react-aria/searchfield": "^3.8.12", "@react-aria/select": "^3.17.3", "@react-aria/selection": "^3.27.2", "@react-aria/separator": "^3.4.16", "@react-aria/slider": "^3.8.5", "@react-aria/ssr": "^3.9.10", "@react-aria/switch": "^3.7.11", "@react-aria/table": "^3.17.11", "@react-aria/tabs": "^3.11.1", "@react-aria/tag": "^3.8.1", "@react-aria/textfield": "^3.18.5", "@react-aria/toast": "^3.0.11", "@react-aria/tooltip": "^3.9.2", "@react-aria/tree": "^3.1.7", "@react-aria/utils": "^3.33.1", "@react-aria/visually-hidden": "^3.8.31", "@react-types/shared": "^3.33.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-nvahimIqdByl/PXk/xPkG30LPRzcin+/Uk0uFfwbbKRRFC9aa22a6BRULZLqVHwa9GaNyKe6CDUxO1Dde4v0kA=="], "react-aria-components": ["react-aria-components@1.16.0", "", { "dependencies": { "@internationalized/date": "^3.12.0", "@internationalized/string": "^3.2.7", "@react-aria/autocomplete": "3.0.0-rc.6", "@react-aria/collections": "^3.0.3", "@react-aria/dnd": "^3.11.6", "@react-aria/focus": "^3.21.5", "@react-aria/interactions": "^3.27.1", "@react-aria/live-announcer": "^3.4.4", "@react-aria/overlays": "^3.31.2", "@react-aria/ssr": "^3.9.10", "@react-aria/textfield": "^3.18.5", "@react-aria/toolbar": "3.0.0-beta.24", "@react-aria/utils": "^3.33.1", "@react-aria/virtualizer": "^4.1.13", "@react-stately/autocomplete": "3.0.0-beta.4", "@react-stately/layout": "^4.6.0", "@react-stately/selection": "^3.20.9", "@react-stately/table": "^3.15.4", "@react-stately/utils": "^3.11.0", "@react-stately/virtualizer": "^4.4.6", "@react-types/form": "^3.7.18", "@react-types/grid": "^3.3.8", "@react-types/shared": "^3.33.1", "@react-types/table": "^3.13.6", "@swc/helpers": "^0.5.0", "client-only": "^0.0.1", "react-aria": "^3.47.0", "react-stately": "^3.45.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-MjHbTLpMFzzD2Tv5KbeXoZwPczuUWZcRavVvQQlNHRtXHH38D+sToMEYpNeir7Wh3K/XWtzeX3EujfJW6QNkrw=="], + "react-avatar-editor": ["react-avatar-editor@15.1.0", "", { "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Zto7u9l6Wd5LPPtjeFJ+7uwoT4bs01OSgkN2kxD18lWl8IiZ0GY3nWCbKPx4qIU7Au1vENsMJm19rfVWHHayaQ=="], + + "react-colorful": ["react-colorful@5.6.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw=="], + "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], + "react-draggable": ["react-draggable@4.5.0", "", { "dependencies": { "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw=="], + + "react-dropzone": ["react-dropzone@12.1.0", "", { "dependencies": { "attr-accept": "^2.2.2", "file-selector": "^0.5.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8" } }, "sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog=="], + + "react-error-boundary": ["react-error-boundary@6.1.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w=="], + + "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], + "react-grab": ["react-grab@0.1.31", "", { "dependencies": { "@react-grab/cli": "0.1.31", "bippy": "^0.5.39" }, "peerDependencies": { "react": ">=17.0.0" }, "optionalPeers": ["react"], "bin": { "react-grab": "bin/cli.js" } }, "sha512-JAdlg46rNFv58l0tGs6omroDlCo1+oj70v03tyaP5AOHbx1wNNP1aaoTcDLSlcN4K5gwve9zR8/t0CT2mwLqSA=="], "react-hook-form": ["react-hook-form@7.72.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig=="], + "react-hotkeys-hook": ["react-hotkeys-hook@5.2.4", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-BgKg+A1+TawkYluh5Bo4cTmcgMN5L29uhJbDUQdHwPX+qgXRjIPYU5kIDHyxnAwCkCBiu9V5OpB2mpyeluVF2A=="], + "react-i18next": ["react-i18next@17.0.2", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.0.1", "react": ">= 16.8.0", "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA=="], + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], + + "react-merge-refs": ["react-merge-refs@3.0.2", "", { "peerDependencies": { "react": ">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["react"] }, "sha512-MSZAfwFfdbEvwkKWP5EI5chuLYnNUxNS7vyS0i1Jp+wtd8J4Ga2ddzhaE68aMol2Z4vCnRM/oGOo1a3V75UPlw=="], + + "react-rnd": ["react-rnd@10.5.3", "", { "dependencies": { "re-resizable": "^6.11.2", "react-draggable": "^4.5.0", "tslib": "2.6.2" }, "peerDependencies": { "react": ">=16.3.0", "react-dom": ">=16.3.0" } }, "sha512-s/sIT3pGZnQ+57egijkTp9mizjIWrJz68Pq6yd+F/wniFY3IriML18dUXnQe/HP9uMiJ+9MAp44hljG99fZu6Q=="], + "react-stately": ["react-stately@3.45.0", "", { "dependencies": { "@react-stately/calendar": "^3.9.3", "@react-stately/checkbox": "^3.7.5", "@react-stately/collections": "^3.12.10", "@react-stately/color": "^3.9.5", "@react-stately/combobox": "^3.13.0", "@react-stately/data": "^3.15.2", "@react-stately/datepicker": "^3.16.1", "@react-stately/disclosure": "^3.0.11", "@react-stately/dnd": "^3.7.4", "@react-stately/form": "^3.2.4", "@react-stately/list": "^3.13.4", "@react-stately/menu": "^3.9.11", "@react-stately/numberfield": "^3.11.0", "@react-stately/overlays": "^3.6.23", "@react-stately/radio": "^3.11.5", "@react-stately/searchfield": "^3.5.19", "@react-stately/select": "^3.9.2", "@react-stately/selection": "^3.20.9", "@react-stately/slider": "^3.7.5", "@react-stately/table": "^3.15.4", "@react-stately/tabs": "^3.8.9", "@react-stately/toast": "^3.1.3", "@react-stately/toggle": "^3.9.5", "@react-stately/tooltip": "^3.5.11", "@react-stately/tree": "^3.9.6", "@react-types/shared": "^3.33.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-G3bYr0BIiookpt4H05VeZUuVS/FslQAj2TeT8vDfCiL314Y+LtPXIPe/a3eamCA0wljy7z1EDYKV50Qbz7pcJg=="], "react-virtuoso": ["react-virtuoso@4.18.4", "", { "peerDependencies": { "react": ">=16 || >=17 || >= 18 || >= 19", "react-dom": ">=16 || >=17 || >= 18 || >=19" } }, "sha512-DNM4Wy2tMA/J6ejMaDdqecOug31rOwgSRg4C/Dw6Iox4dJe9qwcx32M8HdhkE5uHEVVZh7h0koYwAsCSNdxGfQ=="], + "react-zoom-pan-pinch": ["react-zoom-pan-pinch@3.7.0", "", { "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA=="], + + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + + "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], + + "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + + "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + "refa": ["refa@0.12.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.8.0" } }, "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "regexp-ast-analysis": ["regexp-ast-analysis@0.7.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.8.0", "refa": "^0.12.1" } }, "sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A=="], "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], @@ -1284,30 +2033,86 @@ "regjsparser": ["regjsparser@0.13.1", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw=="], + "rehype-github-alerts": ["rehype-github-alerts@4.2.0", "", { "dependencies": { "@primer/octicons": "^19.20.0", "hast-util-from-html": "^2.0.3", "hast-util-is-element": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-6di6kEu9WUHKLKrkKG2xX6AOuaCMGghg0Wq7MEuM/jBYUPVIq6PJpMe00dxMfU+/YSBtDXhffpDimgDi+BObIQ=="], + + "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], + + "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + + "remark-breaks": ["remark-breaks@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="], + + "remark-cjk-friendly": ["remark-cjk-friendly@2.0.1", "", { "dependencies": { "micromark-extension-cjk-friendly": "2.0.1" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-6WwkoQyZf/4j5k53zdFYrR8Ca+UVn992jXdLUSBDZR4eBpFhKyVxmA4gUHra/5fesjGIxrDhHesNr/sVoiiysA=="], + + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-github": ["remark-github@12.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0", "mdast-util-to-string": "^4.0.0", "to-vfile": "^8.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-ByefQKFN184LeiGRCabfl7zUJsdlMYWEhiLX1gpmQ11yFg6xSuOTW7LVCv0oc1x+YvUMJW23NU36sJX2RWGgvg=="], + + "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], + + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "remend": ["remend@1.3.0", "", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="], + + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + "reserved-identifiers": ["reserved-identifiers@1.2.0", "", {}, "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw=="], + "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], + "rolldown": ["rolldown@1.0.0-rc.15", "", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="], "rooks": ["rooks@9.8.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash.debounce": "^4.0.8", "raf": "^3.4.1", "use-sync-external-store": "^1.6.0" }, "optionalDependencies": { "@js-temporal/polyfill": "^0.5.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-S6FqnmERx5zgl8ZUEcnyTe1jgjwE5xeFCgOV4bzgQHKp26P7YA7uPnzzOgacojtoX6E7pQTcewSGqN83wVyz+g=="], + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "screenfull": ["screenfull@5.2.0", "", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="], + + "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], + "scslre": ["scslre@0.3.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.8.0", "refa": "^0.12.0", "regexp-ast-analysis": "^0.7.0" } }, "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ=="], "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], + + "set-value": ["set-value@2.0.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", "is-plain-object": "^2.0.3", "split-string": "^3.0.1" } }, "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="], + + "shiki-stream": ["shiki-stream@0.1.4", "", { "dependencies": { "@shikijs/core": "^3.0.0" }, "peerDependencies": { "react": "^19.0.0", "solid-js": "^1.9.0", "vue": "^3.2.0" }, "optionalPeers": ["react", "solid-js", "vue"] }, "sha512-4pz6JGSDmVTTkPJ/ueixHkFAXY4ySCc+unvCaDZV7hqq/sdJZirRxgIXSuNSKgiFlGTgRR97sdu2R8K55sPsrw=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "simple-icons": ["simple-icons@16.15.0", "", {}, "sha512-hOyY4Cdvh1D/FJa1Qx4nTvypCT2BoI3jpc4xjxVgwVh1Hmd9mnqBqBTziDytCj2f5UOAXCfdnwODiNv710aqkQ=="], @@ -1316,22 +2121,34 @@ "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], "spdx-expression-parse": ["spdx-expression-parse@4.0.0", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ=="], "spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="], + "split-on-first": ["split-on-first@3.0.0", "", {}, "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA=="], + + "split-string": ["split-string@3.1.0", "", { "dependencies": { "extend-shallow": "^3.0.0" } }, "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw=="], + "stable-hash": ["stable-hash@0.0.6", "", {}, "sha512-0afH4mobqTybYZsXImQRLOjHV4gvOW+92HdUIax9t7a8d9v54KWykEuMVIcXhD9BCi+w3kS4x7O6fmZQ3JlG/g=="], "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], + "string-convert": ["string-convert@0.2.1", "", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="], + "string-ts": ["string-ts@2.3.1", "", {}, "sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], @@ -1340,10 +2157,22 @@ "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + + "stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "swr": ["swr@2.4.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="], + "synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="], + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], + "tailwind-csstree": ["tailwind-csstree@0.3.0", "", { "peerDependencies": { "@eslint/css": ">=1.0.0" }, "optionalPeers": ["@eslint/css"] }, "sha512-FJmLCkH1ZDTEqJRVxMVhdiEbk/W67exqvDYrIQ759jsfv3mp3Gp/rrlgtr7dD5q7BK/t8LfaNqXM+BN6BrTKGA=="], "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], @@ -1354,6 +2183,8 @@ "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], + "throttle-debounce": ["throttle-debounce@5.0.2", "", {}, "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="], + "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], @@ -1362,12 +2193,22 @@ "to-valid-identifier": ["to-valid-identifier@1.0.0", "", { "dependencies": { "@sindresorhus/base62": "^1.0.0", "reserved-identifiers": "^1.0.0" } }, "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw=="], + "to-vfile": ["to-vfile@8.0.0", "", { "dependencies": { "vfile": "^6.0.0" } }, "sha512-IcmH1xB5576MJc9qcfEC/m/nQCFt3fzMHz45sSlgJyTWjRbKW1HAkJpuf3DgE57YzIlZcwcBZA5ENQbBo4aLkg=="], + "toml-eslint-parser": ["toml-eslint-parser@1.0.3", "", { "dependencies": { "eslint-visitor-keys": "^5.0.0" } }, "sha512-A5F0cM6+mDleacLIEUkmfpkBbnHJFV1d2rprHU2MXNk7mlxHq2zGojA+SRvQD1RoMo9gqjZPWEaKG4v1BQ48lw=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "ts-declaration-location": ["ts-declaration-location@1.0.7", "", { "dependencies": { "picomatch": "^4.0.2" }, "peerDependencies": { "typescript": ">=4.0.0" } }, "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA=="], + "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + + "ts-md5": ["ts-md5@2.0.1", "", {}, "sha512-yF35FCoEOFBzOclSkMNEUbFQZuv89KEQ+5Xz03HrMSGUGB1+r+El+JiGOFwsP4p9RFNzwlrydYoTLvPOuICl9w=="], + "ts-pattern": ["ts-pattern@5.9.0", "", {}, "sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg=="], "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], @@ -1388,8 +2229,16 @@ "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], @@ -1402,20 +2251,50 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="], + + "use-merge-value": ["use-merge-value@1.2.0", "", { "peerDependencies": { "react": ">= 16.x" } }, "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + + "v8n": ["v8n@1.5.1", "", {}, "sha512-LdabyT4OffkyXFCe9UT+uMkxNBs5rcTVuZClvxQr08D5TUgo1OFKkoT65qYRCsiKBl/usHjpXvP4hHMzzDRj3A=="], + "valibot": ["valibot@1.3.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "virtua": ["virtua@0.49.1", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-6f79msqg3jzNFdqJiS0FSzhRN1EHlDhR7EvW7emp6z5qQ22VdsReiDHflkpMEMhoAyUuYr69nwT0aagiM7NrUg=="], + "vite": ["vite@8.0.8", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + + "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], + + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], + + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + "vue-eslint-parser": ["vue-eslint-parser@10.4.0", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0 || ^9.0.0", "eslint-visitor-keys": "^4.2.0 || ^5.0.0", "espree": "^10.3.0 || ^11.0.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg=="], "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], @@ -1436,12 +2315,28 @@ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + "zustand": ["zustand@3.7.2", "", { "peerDependencies": { "react": ">=16.8" }, "optionalPeers": ["react"] }, "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@emotion/babel-plugin/@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], + + "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + + "@emotion/babel-plugin/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], + + "@emotion/babel-plugin/stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], + + "@emotion/cache/stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], + + "@emotion/serialize/@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], + + "@emotion/serialize/@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], + "@es-joy/jsdoccomment/comment-parser": ["comment-parser@1.4.5", "", {}, "sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw=="], "@es-joy/jsdoccomment/jsdoc-type-pratt-parser": ["jsdoc-type-pratt-parser@7.1.1", "", {}, "sha512-/2uqY7x6bsrpi3i9LVU6J89352C0rpMk0as8trXxCtvd4kPk1ke/Eyif6wqfSLvoNJqcDG9Vk4UsXgygzCt2xA=="], @@ -1454,6 +2349,44 @@ "@heroui/react/tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], + "@lobehub/fluent-emoji/lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], + + "@lobehub/ui/lucide-react": ["lucide-react@1.11.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g=="], + + "@mdx-js/mdx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="], + + "@pierre/diffs/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + + "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-tooltip/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@rc-component/dialog/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], + + "@rc-component/drawer/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], + + "@rc-component/image/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], + + "@rc-component/tour/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], + + "@rc-component/trigger/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], + "@react-grab/cli/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@stylistic/eslint-plugin/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], @@ -1476,6 +2409,16 @@ "clean-regexp/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + "cosmiconfig/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], + + "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], + + "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + "eslint-plugin-jsdoc/@es-joy/jsdoccomment": ["@es-joy/jsdoccomment@0.86.0", "", { "dependencies": { "@types/estree": "^1.0.8", "@typescript-eslint/types": "^8.58.0", "comment-parser": "1.4.6", "esquery": "^1.7.0", "jsdoc-type-pratt-parser": "~7.2.0" } }, "sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw=="], "eslint-plugin-jsonc/@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="], @@ -1488,8 +2431,14 @@ "eslint-plugin-yml/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "estree-util-build-jsx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "extend-shallow/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -1500,16 +2449,66 @@ "mdast-util-frontmatter/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + + "mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "rc-menu/@rc-component/trigger": ["@rc-component/trigger@2.3.1", "", { "dependencies": { "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", "classnames": "^2.3.2", "rc-motion": "^2.0.0", "rc-resize-observer": "^1.3.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A=="], + + "react-rnd/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="], + "rolldown/@oxc-project/types": ["@oxc-project/types@0.124.0", "", {}, "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg=="], "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="], + "set-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "shiki-stream/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "split-string/extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="], + + "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@pierre/diffs/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "@pierre/diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + + "@pierre/diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + + "@pierre/diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "@pierre/diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + + "@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "shiki-stream/@shikijs/core/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], } } diff --git a/crates/desktop/package.json b/crates/desktop/package.json index 727de041..e6ef1246 100644 --- a/crates/desktop/package.json +++ b/crates/desktop/package.json @@ -18,6 +18,7 @@ "@heroicons/react": "^2.2.0", "@heroui/react": "^3.0.1", "@heroui/styles": "^3.0.1", + "@lobehub/icons": "^5.5.4", "@tanstack/react-query": "^5.94.5", "@tauri-apps/api": "^2", "@tauri-apps/plugin-deep-link": "^2.4.7", diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index d6a9d654..e44c7374 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -3,17 +3,17 @@ import { ClipboardDocumentIcon, EyeIcon, EyeSlashIcon, - GlobeAltIcon, PencilIcon, PlusIcon, TrashIcon, } from "@heroicons/react/24/solid"; +import AnthropicIcon from "@lobehub/icons/es/Anthropic"; +import OpenAIIcon from "@lobehub/icons/es/OpenAI"; import { Alert, AlertDialog, Button, Card, - Chip, FieldError, Fieldset, Form, @@ -88,14 +88,36 @@ function formatOption( return option ? t(option.labelKey) : format; } -function ProviderIcon() { +function ProviderIcon({ format }: { format: InferenceProviderFormatDto }) { + const Icon = format === "anthropic" ? AnthropicIcon : OpenAIIcon; + return (
- +
); } +function MonoValue({ + children, + className, +}: { + children: string; + className?: string; +}) { + return ( + + {children} + + ); +} + function ProviderForm({ mode, provider, @@ -524,7 +546,7 @@ function ProviderDetail({
- +

{provider.name} @@ -578,42 +600,23 @@ function ProviderDetail({

- -
-
-

- {t("providerFormat")} -

- - {formatOption(provider.format, t)} - -
-
- -
+ +

{t("providerApiBaseUrl")}

-
- - {provider.api_base_url} - -
+ {provider.api_base_url}
-
+

{t("providerApiKey")}

-
- +
+ {revealedKey ?? "••••••••••••••••••••••••"} - + +
+ +
+ {displayModels.map((model) => ( +
+ + handleChange(model.id, event.target.value) + } + onBlur={onBlur} + placeholder={t("providerModelNamePlaceholder")} + aria-label={t("providerModelName")} + variant="secondary" + className="min-w-0 flex-1" + /> + +
+ ))} +
+ + {errorMessage && ( +

{errorMessage}

+ )} +
+ ); +} + function ProviderForm({ mode, provider, @@ -144,6 +270,7 @@ function ProviderForm({ format: provider?.format ?? "openai_responses", apiBaseUrl: provider?.api_base_url ?? "", apiKey: "", + models: toProviderModelFormValues(provider?.models ?? []), }, }); @@ -169,6 +296,7 @@ function ProviderForm({ const name = values.name.trim(); const apiBaseUrl = values.apiBaseUrl.trim(); const apiKey = values.apiKey.trim(); + const models = normalizeModelNames(values.models); try { if (mode === "create") { @@ -177,6 +305,7 @@ function ProviderForm({ format: values.format, api_base_url: apiBaseUrl, api_key: apiKey, + models, }); toast.success(t("inferenceProviderCreated")); onSuccess(created); @@ -191,6 +320,7 @@ function ProviderForm({ format: values.format, api_base_url: apiBaseUrl, api_key: apiKey || null, + models, }, }); toast.success(t("inferenceProviderUpdated")); @@ -432,6 +562,30 @@ function ProviderForm({ )} /> + + + validateModelNames( + value, + t( + "validationProviderModelNameUnique", + ), + ), + }} + render={({ field, fieldState }) => ( + + )} + /> @@ -534,6 +688,20 @@ function ProviderDetail({ } }; + const handleCopyModel = async (modelName: string) => { + try { + await navigator.clipboard.writeText(modelName); + toast.success(t("providerModelNameCopied")); + } catch (error) { + console.error("Failed to copy inference model name:", error); + toast.danger( + error instanceof Error + ? error.message + : t("providerModelNameCopyFailed"), + ); + } + }; + return ( <>
@@ -639,6 +807,37 @@ function ProviderDetail({
+ +
+

+ {t("providerModels")} +

+ {provider.models.length === 0 ? ( +

+ {t("noProviderModels")} +

+ ) : ( +
+ {provider.models.map((model) => ( + + ))} +
+ )} +
@@ -712,7 +911,10 @@ export default function InferenceProvidersPage() { provider.name.toLowerCase().includes(query) || provider.api_base_url.toLowerCase().includes(query) || provider.format.includes(query) || - format.includes(query) + format.includes(query) || + provider.models.some((model) => + model.toLowerCase().includes(query), + ) ); }); }, [providers, searchQuery, t]); diff --git a/crates/desktop/src/requests/inference-providers.ts b/crates/desktop/src/requests/inference-providers.ts index 7d3dba63..7bf56077 100644 --- a/crates/desktop/src/requests/inference-providers.ts +++ b/crates/desktop/src/requests/inference-providers.ts @@ -59,6 +59,38 @@ export function createInferenceProviderMutationOptions({ }); } +interface CreateInferenceModelVariables { + providerName: string; + modelName: string; +} + +interface CreateInferenceModelMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: ( + data: InferenceProviderResponse, + variables: CreateInferenceModelVariables, + ) => void | Promise; +} + +export function createInferenceModelMutationOptions({ + api, + queryClient, + onSuccess, +}: CreateInferenceModelMutationParams) { + return mutationOptions({ + mutationFn: ({ + providerName, + modelName, + }: CreateInferenceModelVariables) => + api.inferenceProviders.createModel(providerName, modelName), + onSuccess: async (data, variables) => { + await invalidateInferenceProviderQueries(queryClient); + await onSuccess?.(data, variables); + }, + }); +} + interface UpdateInferenceProviderVariables { name: string; body: UpdateInferenceProviderRequest; @@ -88,6 +120,44 @@ export function updateInferenceProviderMutationOptions({ }); } +interface UpdateInferenceModelVariables { + providerName: string; + modelName: string; + nextModelName: string; +} + +interface UpdateInferenceModelMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: ( + data: InferenceProviderResponse, + variables: UpdateInferenceModelVariables, + ) => void | Promise; +} + +export function updateInferenceModelMutationOptions({ + api, + queryClient, + onSuccess, +}: UpdateInferenceModelMutationParams) { + return mutationOptions({ + mutationFn: ({ + providerName, + modelName, + nextModelName, + }: UpdateInferenceModelVariables) => + api.inferenceProviders.updateModel( + providerName, + modelName, + nextModelName, + ), + onSuccess: async (data, variables) => { + await invalidateInferenceProviderQueries(queryClient); + await onSuccess?.(data, variables); + }, + }); +} + interface DeleteInferenceProviderMutationParams { api: ApiClient; queryClient: QueryClient; @@ -107,3 +177,35 @@ export function deleteInferenceProviderMutationOptions({ }, }); } + +interface DeleteInferenceModelVariables { + providerName: string; + modelName: string; +} + +interface DeleteInferenceModelMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: ( + data: InferenceProviderResponse, + variables: DeleteInferenceModelVariables, + ) => void | Promise; +} + +export function deleteInferenceModelMutationOptions({ + api, + queryClient, + onSuccess, +}: DeleteInferenceModelMutationParams) { + return mutationOptions({ + mutationFn: ({ + providerName, + modelName, + }: DeleteInferenceModelVariables) => + api.inferenceProviders.deleteModel(providerName, modelName), + onSuccess: async (data, variables) => { + await invalidateInferenceProviderQueries(queryClient); + await onSuccess?.(data, variables); + }, + }); +} diff --git a/crates/inference/migrations/0002_create_inference_models.sql b/crates/inference/migrations/0002_create_inference_models.sql new file mode 100644 index 00000000..5cc23a6e --- /dev/null +++ b/crates/inference/migrations/0002_create_inference_models.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS inference_models ( + provider_id TEXT NOT NULL, + name TEXT NOT NULL, + PRIMARY KEY (provider_id, name), + FOREIGN KEY (provider_id) + REFERENCES inference_providers(id) + ON DELETE CASCADE +); diff --git a/crates/inference/src/error.rs b/crates/inference/src/error.rs index 88c10982..cb9e7188 100644 --- a/crates/inference/src/error.rs +++ b/crates/inference/src/error.rs @@ -19,6 +19,10 @@ pub enum InferenceProviderError { #[error("provider name cannot be empty")] EmptyName, + /// Model names must not be empty. + #[error("model name cannot be empty")] + EmptyModelName, + /// API keys must not be empty. #[error("provider API key cannot be empty")] EmptyApiKey, @@ -31,6 +35,10 @@ pub enum InferenceProviderError { #[error("provider already exists: {0}")] AlreadyExists(String), + /// Model name is already in use for the provider. + #[error("inference model already exists: {0}")] + ModelAlreadyExists(String), + /// Provider format is not supported. #[error("unsupported inference provider format: {0}")] InvalidFormat(String), diff --git a/crates/inference/src/model.rs b/crates/inference/src/model.rs index a05aa24d..cff5bd87 100644 --- a/crates/inference/src/model.rs +++ b/crates/inference/src/model.rs @@ -76,6 +76,9 @@ pub struct InferenceProvider { /// Base URL for provider API requests. pub api_base_url: String, + + /// Model names supported by this provider. + pub models: Vec, } /// Provider creation input. @@ -92,6 +95,10 @@ pub struct CreateInferenceProvider { /// Provider API key. This is write-only and never stored in JSON. pub api_key: String, + + /// Model names supported by this provider. + #[serde(default)] + pub models: Vec, } impl fmt::Debug for CreateInferenceProvider { @@ -100,6 +107,7 @@ impl fmt::Debug for CreateInferenceProvider { .field("name", &self.name) .field("format", &self.format) .field("api_base_url", &self.api_base_url) + .field("models", &self.models) .field("api_key", &"[redacted]") .finish() } @@ -119,6 +127,9 @@ pub struct UpdateInferenceProvider { /// Updated provider API key. This is write-only and never stored in JSON. pub api_key: Option, + + /// Updated model names supported by this provider. + pub models: Option>, } impl fmt::Debug for UpdateInferenceProvider { @@ -127,6 +138,7 @@ impl fmt::Debug for UpdateInferenceProvider { .field("name", &self.name) .field("format", &self.format) .field("api_base_url", &self.api_base_url) + .field("models", &self.models) .field("api_key", &self.api_key.as_ref().map(|_| "[redacted]")) .finish() } diff --git a/crates/inference/src/store.rs b/crates/inference/src/store.rs index 4e48580d..21ff679c 100644 --- a/crates/inference/src/store.rs +++ b/crates/inference/src/store.rs @@ -1,5 +1,6 @@ //! SQLite-backed CRUD storage for inference providers. +use std::collections::HashSet; use std::future::Future; use std::path::{Path, PathBuf}; @@ -130,6 +131,9 @@ impl InferenceProviderStore { .create_if_missing(true) .connect() .await?; + sqlx::query("PRAGMA foreign_keys = ON") + .execute(&mut conn) + .await?; sqlx::migrate!().run(&mut conn).await?; Ok(conn) } @@ -143,12 +147,15 @@ impl InferenceProviderStore { FROM inference_providers WHERE id = ?", ) .bind(id) - .fetch_optional(conn) + .fetch_optional(&mut *conn) .await?; - row.map(map_row) + let mut provider = row + .map(map_row) .transpose()? - .ok_or_else(|| InferenceProviderError::NotFound(id.to_string())) + .ok_or_else(|| InferenceProviderError::NotFound(id.to_string()))?; + provider.models = Self::fetch_model_names(conn, &provider.id).await?; + Ok(provider) } async fn check_name_unique( @@ -181,6 +188,47 @@ impl InferenceProviderStore { Ok(()) } } + + async fn fetch_model_names( + conn: &mut SqliteConnection, + provider_id: &str, + ) -> Result> { + let rows = sqlx::query( + "SELECT name FROM inference_models \ + WHERE provider_id = ? ORDER BY rowid", + ) + .bind(provider_id) + .fetch_all(conn) + .await?; + + rows.into_iter() + .map(|row| row.try_get("name").map_err(Into::into)) + .collect() + } + + async fn replace_models( + conn: &mut SqliteConnection, + provider_id: &str, + models: &[String], + ) -> Result<()> { + sqlx::query("DELETE FROM inference_models WHERE provider_id = ?") + .bind(provider_id) + .execute(&mut *conn) + .await?; + + for model in models { + sqlx::query( + "INSERT INTO inference_models (provider_id, name) \ + VALUES (?, ?)", + ) + .bind(provider_id) + .bind(model) + .execute(&mut *conn) + .await?; + } + + Ok(()) + } } fn map_row(row: sqlx::sqlite::SqliteRow) -> Result { @@ -191,6 +239,7 @@ fn map_row(row: sqlx::sqlite::SqliteRow) -> Result { name: row.try_get("name")?, format, api_base_url: row.try_get("api_base_url")?, + models: Vec::new(), }) } @@ -206,7 +255,14 @@ impl InferenceProviderRepository ) .fetch_all(&mut conn) .await?; - rows.into_iter().map(map_row).collect() + let mut providers = Vec::with_capacity(rows.len()); + for row in rows { + let mut provider = map_row(row)?; + provider.models = + Self::fetch_model_names(&mut conn, &provider.id).await?; + providers.push(provider); + } + Ok(providers) }) } @@ -223,6 +279,7 @@ impl InferenceProviderRepository ) -> Result { let name = clean_name(&input.name)?; let api_base_url = clean_api_base_url(&input.api_base_url)?; + let models = clean_model_names(&input.models)?; ensure_api_key(&input.api_key)?; self.block_on(async { @@ -234,24 +291,37 @@ impl InferenceProviderRepository name, format: input.format, api_base_url, + models, }; self.credentials.set_api_key(&provider.id, &input.api_key)?; - let result = sqlx::query( - "INSERT INTO inference_providers (id, name, format, api_base_url) \ - VALUES (?, ?, ?, ?)", - ) - .bind(&provider.id) - .bind(&provider.name) - .bind(provider.format.to_string()) - .bind(&provider.api_base_url) - .execute(&mut conn) + let result: Result<()> = async { + sqlx::query( + "INSERT INTO inference_providers \ + (id, name, format, api_base_url) \ + VALUES (?, ?, ?, ?)", + ) + .bind(&provider.id) + .bind(&provider.name) + .bind(provider.format.to_string()) + .bind(&provider.api_base_url) + .execute(&mut conn) + .await?; + Self::replace_models(&mut conn, &provider.id, &provider.models) + .await?; + Ok(()) + } .await; if let Err(error) = result { let _ = self.credentials.delete_api_key(&provider.id); - return Err(error.into()); + let _ = + sqlx::query("DELETE FROM inference_providers WHERE id = ?") + .bind(&provider.id) + .execute(&mut conn) + .await; + return Err(error); } Ok(provider) @@ -263,6 +333,12 @@ impl InferenceProviderRepository id: &str, input: UpdateInferenceProvider, ) -> Result { + let models = input + .models + .as_ref() + .map(|models| clean_model_names(models)) + .transpose()?; + self.block_on(async { let mut conn = self.open_db().await?; let mut provider = Self::fetch_by_id(&mut conn, id).await?; @@ -281,6 +357,10 @@ impl InferenceProviderRepository provider.api_base_url = clean_api_base_url(api_base_url)?; } + if let Some(models) = models { + provider.models = models; + } + let previous_api_key = match input.api_key.as_ref() { Some(api_key) => { ensure_api_key(api_key)?; @@ -291,16 +371,24 @@ impl InferenceProviderRepository None => None, }; - let result = sqlx::query( - "UPDATE inference_providers \ - SET name = ?, format = ?, api_base_url = ? \ - WHERE id = ?", - ) - .bind(&provider.name) - .bind(provider.format.to_string()) - .bind(&provider.api_base_url) - .bind(id) - .execute(&mut conn) + let result: Result<()> = async { + sqlx::query( + "UPDATE inference_providers \ + SET name = ?, format = ?, api_base_url = ? \ + WHERE id = ?", + ) + .bind(&provider.name) + .bind(provider.format.to_string()) + .bind(&provider.api_base_url) + .bind(id) + .execute(&mut conn) + .await?; + if input.models.is_some() { + Self::replace_models(&mut conn, id, &provider.models) + .await?; + } + Ok(()) + } .await; if let Err(error) = result { @@ -314,7 +402,7 @@ impl InferenceProviderRepository } } } - return Err(error.into()); + return Err(error); } Ok(provider) @@ -363,6 +451,30 @@ impl InferenceProviderRepository } } +fn clean_model_name(name: &str) -> Result { + let name = name.trim(); + if name.is_empty() { + Err(InferenceProviderError::EmptyModelName) + } else { + Ok(name.to_string()) + } +} + +fn clean_model_names(models: &[String]) -> Result> { + let mut seen = HashSet::new(); + let mut clean = Vec::with_capacity(models.len()); + + for model in models { + let model = clean_model_name(model)?; + if !seen.insert(model.to_ascii_lowercase()) { + return Err(InferenceProviderError::ModelAlreadyExists(model)); + } + clean.push(model); + } + + Ok(clean) +} + fn clean_name(name: &str) -> Result { let name = name.trim(); if name.is_empty() { @@ -433,6 +545,21 @@ mod tests { (temp, store) } + fn create_provider( + store: &InferenceProviderStore, + name: &str, + ) -> InferenceProvider { + store + .create(CreateInferenceProvider { + name: name.to_string(), + format: InferenceProviderFormat::OpenAiResponses, + api_base_url: "https://api.openai.com/v1".to_string(), + api_key: "secret".to_string(), + models: Vec::new(), + }) + .unwrap() + } + #[test] fn test_list_missing_file_is_empty() { let (_temp, store) = store(); @@ -450,6 +577,7 @@ mod tests { format: InferenceProviderFormat::OpenAiResponses, api_base_url: "https://api.openai.com/v1".to_string(), api_key: "sk-test".to_string(), + models: Vec::new(), }) .unwrap(); @@ -458,6 +586,7 @@ mod tests { assert_eq!(fetched.name, "OpenAI"); assert_eq!(fetched.format, InferenceProviderFormat::OpenAiResponses); assert_eq!(fetched.api_base_url, "https://api.openai.com/v1"); + assert!(fetched.models.is_empty()); // API key is kept in the credential store, not in provider metadata assert!(store @@ -480,6 +609,7 @@ mod tests { format: InferenceProviderFormat::Anthropic, api_base_url: "https://api.anthropic.com/v1".to_string(), api_key: "first-key".to_string(), + models: Vec::new(), }) .unwrap(); @@ -493,6 +623,7 @@ mod tests { "https://gateway.example.com/v1".to_string(), ), api_key: Some("second-key".to_string()), + models: None, }, ) .unwrap(); @@ -515,6 +646,7 @@ mod tests { format: InferenceProviderFormat::Anthropic, api_base_url: "https://api.anthropic.com/v1".to_string(), api_key: "secret".to_string(), + models: Vec::new(), }) .unwrap(); @@ -534,6 +666,7 @@ mod tests { format: InferenceProviderFormat::OpenAiResponses, api_base_url: "https://api.openai.com/v1".to_string(), api_key: "first".to_string(), + models: Vec::new(), }) .unwrap(); @@ -543,6 +676,7 @@ mod tests { format: InferenceProviderFormat::OpenAiCompletions, api_base_url: "https://gateway.example.com/v1".to_string(), api_key: "second".to_string(), + models: Vec::new(), }) .unwrap_err(); @@ -559,9 +693,135 @@ mod tests { format: InferenceProviderFormat::OpenAiResponses, api_base_url: " ".to_string(), api_key: "secret".to_string(), + models: Vec::new(), }) .unwrap_err(); assert!(matches!(error, InferenceProviderError::EmptyApiBaseUrl)); } + + #[test] + fn test_create_and_list_provider_models() { + let (_temp, store) = store(); + let provider = store + .create(CreateInferenceProvider { + name: "OpenAI".to_string(), + format: InferenceProviderFormat::OpenAiResponses, + api_base_url: "https://api.openai.com/v1".to_string(), + api_key: "secret".to_string(), + models: vec![ + " gpt-5.4 ".to_string(), + "gpt-5.4-mini".to_string(), + ], + }) + .unwrap(); + + assert_eq!( + provider.models, + vec!["gpt-5.4".to_string(), "gpt-5.4-mini".to_string()] + ); + assert_eq!(store.get(&provider.id).unwrap().models, provider.models); + assert_eq!(store.list().unwrap()[0].models, provider.models); + } + + #[test] + fn test_provider_model_names_are_unique_case_insensitive() { + let (_temp, store) = store(); + + let error = store + .create(CreateInferenceProvider { + name: "OpenAI".to_string(), + format: InferenceProviderFormat::OpenAiResponses, + api_base_url: "https://api.openai.com/v1".to_string(), + api_key: "secret".to_string(), + models: vec!["gpt-5.4".to_string(), "GPT-5.4".to_string()], + }) + .unwrap_err(); + + assert!(matches!( + error, + InferenceProviderError::ModelAlreadyExists(_) + )); + } + + #[test] + fn test_update_and_delete_provider_model() { + let (_temp, store) = store(); + let provider = create_provider(&store, "OpenAI"); + + let updated = store + .update( + &provider.id, + UpdateInferenceProvider { + name: None, + format: None, + api_base_url: None, + api_key: None, + models: Some(vec!["gpt-5.5".to_string()]), + }, + ) + .unwrap(); + + assert_eq!(updated.models, vec!["gpt-5.5".to_string()]); + assert_eq!(store.get(&provider.id).unwrap().models, updated.models); + + let updated = store + .update( + &provider.id, + UpdateInferenceProvider { + name: None, + format: None, + api_base_url: None, + api_key: None, + models: Some(Vec::new()), + }, + ) + .unwrap(); + + assert!(updated.models.is_empty()); + assert!(store.get(&provider.id).unwrap().models.is_empty()); + } + + #[test] + fn test_delete_provider_cascades_models() { + let (_temp, store) = store(); + let provider = store + .create(CreateInferenceProvider { + name: "OpenAI".to_string(), + format: InferenceProviderFormat::OpenAiResponses, + api_base_url: "https://api.openai.com/v1".to_string(), + api_key: "secret".to_string(), + models: vec!["gpt-5.4".to_string()], + }) + .unwrap(); + + store.delete(&provider.id).unwrap(); + + store.block_on(async { + let mut conn = store.open_db().await.unwrap(); + let count: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM inference_models") + .fetch_one(&mut conn) + .await + .unwrap(); + assert_eq!(count, 0); + }); + } + + #[test] + fn test_empty_model_name_is_rejected() { + let (_temp, store) = store(); + + let error = store + .create(CreateInferenceProvider { + name: "OpenAI".to_string(), + format: InferenceProviderFormat::OpenAiResponses, + api_base_url: "https://api.openai.com/v1".to_string(), + api_key: "secret".to_string(), + models: vec![" ".to_string()], + }) + .unwrap_err(); + + assert!(matches!(error, InferenceProviderError::EmptyModelName)); + } } From 4bb829d557b62e187412404ea9f740d086ceadf5 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sun, 26 Apr 2026 11:42:16 +0800 Subject: [PATCH 14/62] feat(inference): split provider resource list --- crates/desktop/src/lib/locales/en.ts | 3 + crates/desktop/src/lib/locales/zh-Hans.ts | 3 + crates/desktop/src/lib/locales/zh-Hant.ts | 3 + .../desktop/src/pages/inference-providers.tsx | 174 ++++++++++++++---- 4 files changed, 144 insertions(+), 39 deletions(-) diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index f4738aa0..7e0c1348 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -92,6 +92,8 @@ export default { codeEditorsDescription: "Choose your preferred code editor for opening files", inferenceProviders: "Inference Providers", + codingAgents: "Coding Agents", + searchInferenceProviderResources: "Search agents or providers...", searchInferenceProviders: "Search providers...", refreshInferenceProviders: "Refresh providers", createInferenceProvider: "Add Provider", @@ -103,6 +105,7 @@ export default { deleteInferenceProvider: "Delete Provider", deleteInferenceProviderConfirm: 'Delete "{{name}}"? The stored API key will also be removed.', + noCodingAgentsMatch: "No coding agents match", noInferenceProviders: "No inference providers yet.", noInferenceProvidersMatch: "No providers match", providerName: "Name", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index c75f08c2..9965e748 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -88,6 +88,8 @@ export default { codeEditors: "代码编辑器", codeEditorsDescription: "选择用于打开文件的首选代码编辑器", inferenceProviders: "推理 Provider", + codingAgents: "Coding Agents", + searchInferenceProviderResources: "搜索 Agent 或 Provider...", searchInferenceProviders: "搜索 Provider...", refreshInferenceProviders: "刷新 Provider", createInferenceProvider: "添加 Provider", @@ -99,6 +101,7 @@ export default { deleteInferenceProvider: "删除 Provider", deleteInferenceProviderConfirm: '确定要删除"{{name}}"吗?已存储的 API key 也会被移除。', + noCodingAgentsMatch: "没有匹配的 Coding Agent", noInferenceProviders: "暂无推理 Provider。", noInferenceProvidersMatch: "没有匹配的 Provider", providerName: "名称", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index 1769c541..53b97797 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -88,6 +88,8 @@ export default { codeEditors: "程式碼編輯器", codeEditorsDescription: "選擇用於開啟檔案的偏好程式碼編輯器", inferenceProviders: "推理 Provider", + codingAgents: "Coding Agents", + searchInferenceProviderResources: "搜尋 Agent 或 Provider...", searchInferenceProviders: "搜尋 Provider...", refreshInferenceProviders: "重新整理 Provider", createInferenceProvider: "新增 Provider", @@ -99,6 +101,7 @@ export default { deleteInferenceProvider: "刪除 Provider", deleteInferenceProviderConfirm: "確定要刪除「{{name}}」嗎?已儲存的 API key 也會被移除。", + noCodingAgentsMatch: "沒有符合的 Coding Agent", noInferenceProviders: "尚無推理 Provider。", noInferenceProvidersMatch: "沒有符合的 Provider", providerName: "名稱", diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index e460bcd9..0309f6f8 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -1,10 +1,12 @@ import { ArrowPathIcon, ClipboardDocumentIcon, + CpuChipIcon, EyeIcon, EyeSlashIcon, PencilIcon, PlusIcon, + ServerIcon, TrashIcon, } from "@heroicons/react/24/solid"; import AnthropicIcon from "@lobehub/icons/es/Anthropic"; @@ -27,15 +29,18 @@ import { toast, } from "@heroui/react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import Fuse from "fuse.js"; import { useMemo, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { ListSearchHeader } from "../components/list-search-header"; +import { ResourceSectionHeader } from "../components/resource-section-header"; import type { InferenceProviderFormatDto, InferenceProviderResponse, } from "../generated/dto"; import { useApi } from "../hooks/use-api"; +import { AgentIcon } from "../lib/agent-icons"; import { cn } from "../lib/utils"; import { createInferenceProviderMutationOptions, @@ -49,6 +54,13 @@ type PanelMode = | { type: "create" } | { type: "edit"; provider: InferenceProviderResponse }; +type CodingAgentId = "opencode" | "codex"; + +interface CodingAgentOption { + id: CodingAgentId; + label: string; +} + interface FormatOption { id: InferenceProviderFormatDto; labelKey: string; @@ -73,6 +85,17 @@ const FORMAT_OPTIONS: FormatOption[] = [ }, ]; +const CODING_AGENT_OPTIONS: CodingAgentOption[] = [ + { + id: "opencode", + label: "OpenCode", + }, + { + id: "codex", + label: "Codex", + }, +]; + interface InferenceProviderFormValues { name: string; format: InferenceProviderFormatDto; @@ -97,14 +120,6 @@ function toProviderModelFormValues(models: string[]) { return models.map((model) => createProviderModelFormValue(model)); } -function formatOption( - format: InferenceProviderFormatDto, - t: (key: string) => string, -) { - const option = FORMAT_OPTIONS.find((item) => item.id === format); - return option ? t(option.labelKey) : format; -} - function ProviderIcon({ format }: { format: InferenceProviderFormatDto }) { const Icon = format === "anthropic" ? AnthropicIcon : OpenAIIcon; @@ -822,7 +837,7 @@ function ProviderDetail({
From 5cd60a2eadd873d3c579989cb222ff8cfbab65fa Mon Sep 17 00:00:00 2001 From: akarachen Date: Sun, 26 Apr 2026 11:53:44 +0800 Subject: [PATCH 15/62] feat(inference): add coding agent panels --- .../desktop/src/pages/inference-providers.tsx | 42 ++++++++++++++----- .../pages/inference-providers/codex-panel.tsx | 29 +++++++++++++ .../inference-providers/opencode-panel.tsx | 29 +++++++++++++ 3 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 crates/desktop/src/pages/inference-providers/codex-panel.tsx create mode 100644 crates/desktop/src/pages/inference-providers/opencode-panel.tsx diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index 0309f6f8..48b2e68a 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -42,6 +42,8 @@ import type { import { useApi } from "../hooks/use-api"; import { AgentIcon } from "../lib/agent-icons"; import { cn } from "../lib/utils"; +import { CodexInferenceProviderPanel } from "./inference-providers/codex-panel"; +import { OpenCodeInferenceProviderPanel } from "./inference-providers/opencode-panel"; import { createInferenceProviderMutationOptions, deleteInferenceProviderMutationOptions, @@ -49,12 +51,13 @@ import { updateInferenceProviderMutationOptions, } from "../requests/inference-providers"; +type CodingAgentId = "opencode" | "codex"; + type PanelMode = | { type: "detail" } | { type: "create" } - | { type: "edit"; provider: InferenceProviderResponse }; - -type CodingAgentId = "opencode" | "codex"; + | { type: "edit"; provider: InferenceProviderResponse } + | { type: "agent"; agentId: CodingAgentId }; interface CodingAgentOption { id: CodingAgentId; @@ -904,8 +907,6 @@ export default function InferenceProvidersPage() { const { t } = useTranslation(); const api = useApi(); const [searchQuery, setSearchQuery] = useState(""); - const [selectedAgentId, setSelectedAgentId] = - useState("opencode"); const [selectedName, setSelectedName] = useState(null); const [panel, setPanel] = useState({ type: "detail" }); @@ -971,11 +972,14 @@ export default function InferenceProvidersPage() { }, [providers, selectedName]); const selectedAgentKeys = useMemo(() => { - return new Set([selectedAgentId]); - }, [selectedAgentId]); + return panel.type === "agent" + ? new Set([panel.agentId]) + : new Set(); + }, [panel]); const selectedProviderKeys = useMemo(() => { - return activeProvider && panel.type !== "create" + return activeProvider && + (panel.type === "detail" || panel.type === "edit") ? new Set([activeProvider.name]) : new Set(); }, [activeProvider, panel.type]); @@ -1056,10 +1060,18 @@ export default function InferenceProvidersPage() { - setSelectedAgentId(key as CodingAgentId) - } + onSelectionChange={(keys) => { + if (keys === "all") return; + const agentId = [...keys][0] as + | CodingAgentId + | undefined; + if (!agentId) return; + + setSelectedName(null); + setPanel({ type: "agent", agentId }); + }} className="p-2" > {filteredCodingAgents.map((agent) => ( @@ -1136,6 +1148,14 @@ export default function InferenceProvidersPage() {
+ {panel.type === "agent" && panel.agentId === "opencode" && ( + + )} + + {panel.type === "agent" && panel.agentId === "codex" && ( + + )} + {panel.type === "create" && ( +
+ + +
+ +
+

+ Codex +

+
+
+
+ +
+
+
+ ); +} diff --git a/crates/desktop/src/pages/inference-providers/opencode-panel.tsx b/crates/desktop/src/pages/inference-providers/opencode-panel.tsx new file mode 100644 index 00000000..a3095872 --- /dev/null +++ b/crates/desktop/src/pages/inference-providers/opencode-panel.tsx @@ -0,0 +1,29 @@ +import { Card } from "@heroui/react"; +import { AgentIcon } from "../../lib/agent-icons"; + +export function OpenCodeInferenceProviderPanel() { + return ( +
+
+ + +
+ +
+

+ OpenCode +

+
+
+
+ +
+
+
+ ); +} From b57ad6a8ee820f89efd5c88ea0a13902958dfbd3 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sun, 26 Apr 2026 13:27:40 +0800 Subject: [PATCH 16/62] feat(inference): add agent provider abstractions --- crates/api/src/error.rs | 6 +- crates/inference/src/agent.rs | 801 ++++++++++++++++++++++++++++++++++ crates/inference/src/error.rs | 13 + crates/inference/src/lib.rs | 8 + 4 files changed, 827 insertions(+), 1 deletion(-) create mode 100644 crates/inference/src/agent.rs diff --git a/crates/api/src/error.rs b/crates/api/src/error.rs index 53b6330c..9f1bdcfe 100644 --- a/crates/api/src/error.rs +++ b/crates/api/src/error.rs @@ -87,10 +87,14 @@ impl From for ApiError { fn from(e: InferenceProviderError) -> Self { match e { InferenceProviderError::EmptyName + | InferenceProviderError::EmptyAgentProviderId | InferenceProviderError::EmptyModelName | InferenceProviderError::EmptyApiBaseUrl | InferenceProviderError::EmptyApiKey - | InferenceProviderError::InvalidFormat(_) => ApiError::new( + | InferenceProviderError::InvalidFormat(_) + | InferenceProviderError::UnsupportedAgentProviderCapability { + .. + } => ApiError::new( Status::BadRequest, e.to_string(), "INVALID_PARAM", diff --git a/crates/inference/src/agent.rs b/crates/inference/src/agent.rs new file mode 100644 index 00000000..4f4e59a5 --- /dev/null +++ b/crates/inference/src/agent.rs @@ -0,0 +1,801 @@ +//! Agent-facing provider management abstractions. +//! +//! `InferenceProvider` is aghub's provider inventory. The types in this +//! module describe how an agent can consume that inventory: some agents expose +//! only one closed credential slot, while others expose a provider registry +//! with explicit default model/provider selection. + +use std::fmt; + +use serde::{Deserialize, Serialize}; + +use crate::error::{InferenceProviderError, Result}; +use crate::model::{InferenceProvider, InferenceProviderFormat}; + +/// How provider definitions are represented by an agent. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AgentProviderMode { + /// The agent has one implicit provider slot. + /// + /// Claude Code is the canonical example: provider-ish configuration is + /// expressed through environment variables such as `ANTHROPIC_API_KEY`, + /// not through a named provider registry. + ClosedSingle, + + /// The agent has a named provider registry. + /// + /// OpenCode and Codex are examples. The registry can contain custom + /// providers, and may also coexist with built-in providers. + Registry, +} + +/// Built-in provider behavior for registry-style agents. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuiltInProviderSupport { + /// Whether the agent ships built-in providers outside user config. + pub available: bool, + + /// Whether user config can redefine a built-in provider id. + pub override_supported: bool, +} + +impl BuiltInProviderSupport { + /// No built-in provider registry is exposed to aghub. + pub const NONE: Self = Self { + available: false, + override_supported: false, + }; + + /// Built-ins exist, but their definitions are immutable. + pub const IMMUTABLE: Self = Self { + available: true, + override_supported: false, + }; + + /// Built-ins exist and can be overridden. + pub const OVERRIDABLE: Self = Self { + available: true, + override_supported: true, + }; +} + +/// Default model/provider selection behavior. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct AgentProviderDefaultSupport { + /// Agent can persist a default provider id separately from the model. + pub provider: bool, + + /// Agent can persist a primary/default model. + pub model: bool, + + /// Agent can persist a smaller/faster model for background work. + pub small_model: bool, + + /// Agent default model values may encode provider and model together. + /// + /// OpenCode uses provider-qualified model ids such as + /// `anthropic/claude-sonnet-4-5`. + pub provider_qualified_model: bool, +} + +impl AgentProviderDefaultSupport { + /// No default selection is manageable. + pub const NONE: Self = Self { + provider: false, + model: false, + small_model: false, + provider_qualified_model: false, + }; + + /// One default model without a separate provider selector. + pub const MODEL_ONLY: Self = Self { + provider: false, + model: true, + small_model: false, + provider_qualified_model: false, + }; + + /// OpenCode-style default and small model routes. + pub const QUALIFIED_MODELS: Self = Self { + provider: false, + model: true, + small_model: true, + provider_qualified_model: true, + }; + + /// Codex-style `model_provider` plus `model`. + pub const PROVIDER_AND_MODEL: Self = Self { + provider: true, + model: true, + small_model: false, + provider_qualified_model: false, + }; +} + +/// Where an agent can read provider credentials from. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct AgentCredentialSupport { + /// Config can reference an environment variable that contains the key. + pub env_var_reference: bool, + + /// Config can store an API key inline. + pub inline_api_key: bool, + + /// The agent has its own credential store or login/connect command. + pub agent_credential_store: bool, +} + +impl AgentCredentialSupport { + /// Environment variables only. + pub const ENV_VAR: Self = Self { + env_var_reference: true, + inline_api_key: false, + agent_credential_store: false, + }; + + /// Environment variables or inline config values. + pub const ENV_VAR_OR_INLINE: Self = Self { + env_var_reference: true, + inline_api_key: true, + agent_credential_store: false, + }; + + /// Environment variables plus the agent's own credential store. + pub const ENV_VAR_OR_AGENT_STORE: Self = Self { + env_var_reference: true, + inline_api_key: false, + agent_credential_store: true, + }; +} + +/// Capability profile for one agent's provider management surface. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct AgentProviderCapabilities { + /// Provider representation mode. + pub mode: AgentProviderMode, + + /// Whether custom provider definitions can be created. + pub custom_providers: bool, + + /// Built-in provider behavior. + pub built_ins: BuiltInProviderSupport, + + /// Default provider/model selection behavior. + pub defaults: AgentProviderDefaultSupport, + + /// Credential placement behavior. + pub credentials: AgentCredentialSupport, +} + +impl AgentProviderCapabilities { + /// Closed single-slot agent such as Claude Code. + pub const fn closed_single( + defaults: AgentProviderDefaultSupport, + credentials: AgentCredentialSupport, + ) -> Self { + Self { + mode: AgentProviderMode::ClosedSingle, + custom_providers: false, + built_ins: BuiltInProviderSupport::NONE, + defaults, + credentials, + } + } + + /// Registry-style agent such as OpenCode or Codex. + pub const fn registry( + defaults: AgentProviderDefaultSupport, + credentials: AgentCredentialSupport, + built_ins: BuiltInProviderSupport, + ) -> Self { + Self { + mode: AgentProviderMode::Registry, + custom_providers: true, + built_ins, + defaults, + credentials, + } + } + + /// Check whether a capability is supported. + pub fn supports(&self, capability: AgentProviderCapability) -> bool { + match capability { + AgentProviderCapability::CustomProviders => self.custom_providers, + AgentProviderCapability::MultipleProviders => { + self.mode == AgentProviderMode::Registry + } + AgentProviderCapability::BuiltInProviders => { + self.built_ins.available + } + AgentProviderCapability::OverrideBuiltInProviders => { + self.built_ins.override_supported + } + AgentProviderCapability::DefaultProvider => self.defaults.provider, + AgentProviderCapability::DefaultModel => self.defaults.model, + AgentProviderCapability::SmallModel => self.defaults.small_model, + AgentProviderCapability::ProviderQualifiedModel => { + self.defaults.provider_qualified_model + } + AgentProviderCapability::EnvVarCredentials => { + self.credentials.env_var_reference + } + AgentProviderCapability::InlineApiKeyCredentials => { + self.credentials.inline_api_key + } + AgentProviderCapability::AgentCredentialStore => { + self.credentials.agent_credential_store + } + } + } + + /// Return an error when the agent lacks a capability. + pub fn ensure_supports( + &self, + agent_id: &str, + capability: AgentProviderCapability, + ) -> Result<()> { + if self.supports(capability) { + Ok(()) + } else { + Err(InferenceProviderError::UnsupportedAgentProviderCapability { + agent_id: agent_id.to_string(), + capability: capability.to_string(), + }) + } + } +} + +/// Individual capability flags used for validation and UI affordances. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AgentProviderCapability { + /// Agent can define custom providers. + CustomProviders, + + /// Agent can manage more than one custom provider. + MultipleProviders, + + /// Agent has built-in providers. + BuiltInProviders, + + /// Agent lets user config redefine built-in provider ids. + OverrideBuiltInProviders, + + /// Agent can persist a default provider id. + DefaultProvider, + + /// Agent can persist a default model. + DefaultModel, + + /// Agent can persist a small/background model. + SmallModel, + + /// Agent uses provider-qualified model ids for model selection. + ProviderQualifiedModel, + + /// Agent can reference API keys by environment variable. + EnvVarCredentials, + + /// Agent can store API keys inline in config. + InlineApiKeyCredentials, + + /// Agent has a native credential store. + AgentCredentialStore, +} + +impl fmt::Display for AgentProviderCapability { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let value = match self { + Self::CustomProviders => "custom providers", + Self::MultipleProviders => "multiple providers", + Self::BuiltInProviders => "built-in providers", + Self::OverrideBuiltInProviders => "built-in provider override", + Self::DefaultProvider => "default provider", + Self::DefaultModel => "default model", + Self::SmallModel => "small model", + Self::ProviderQualifiedModel => "provider-qualified model", + Self::EnvVarCredentials => "environment variable credentials", + Self::InlineApiKeyCredentials => "inline API key credentials", + Self::AgentCredentialStore => "agent credential store", + }; + f.write_str(value) + } +} + +/// Credential reference persisted in an agent config. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AgentProviderCredential { + /// No credential is configured or the provider does not need one. + None, + + /// Config points at an environment variable. + EnvVar { name: String }, + + /// The agent stores the credential internally. + AgentStore { id: Option }, + + /// The key is stored inline and must be treated as redacted. + Inline, +} + +/// Origin of a provider binding inside an agent. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AgentProviderSource { + /// One implicit provider slot with no registry key. + ClosedSlot, + + /// Built-in provider supplied by the agent. + BuiltIn, + + /// Custom provider from user configuration. + Custom, +} + +/// Model entry attached to an agent provider. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AgentProviderModel { + /// Provider-local model id. + pub id: String, + + /// Optional display label. + pub name: Option, +} + +impl AgentProviderModel { + /// Create a model entry using the id as the only stable value. + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + name: None, + } + } +} + +impl From for AgentProviderModel { + fn from(id: String) -> Self { + Self::new(id) + } +} + +impl From<&str> for AgentProviderModel { + fn from(id: &str) -> Self { + Self::new(id) + } +} + +/// Provider definition as seen by one agent. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AgentProviderBinding { + /// Agent-local provider id. + /// + /// For registry agents this is the provider key. For closed-slot agents, + /// use a stable synthetic id such as `primary`. + pub id: String, + + /// Optional id of the source `InferenceProvider` in aghub's inventory. + pub source_provider_id: Option, + + /// User-visible provider name. + pub name: String, + + /// Request/response wire format. + pub format: InferenceProviderFormat, + + /// API base URL, if the agent exposes one. + pub api_base_url: Option, + + /// Credential reference used by the agent. + pub credential: AgentProviderCredential, + + /// Models registered for this provider. + #[serde(default)] + pub models: Vec, + + /// Where this binding came from. + pub source: AgentProviderSource, +} + +impl AgentProviderBinding { + /// Build an agent binding from an aghub provider inventory entry. + pub fn from_inventory( + id: impl Into, + provider: &InferenceProvider, + credential: AgentProviderCredential, + source: AgentProviderSource, + ) -> Result { + let id = clean_agent_provider_id(id.into())?; + Ok(Self { + id, + source_provider_id: Some(provider.id.clone()), + name: provider.name.clone(), + format: provider.format, + api_base_url: Some(provider.api_base_url.clone()), + credential, + models: provider + .models + .iter() + .cloned() + .map(AgentProviderModel::from) + .collect(), + source, + }) + } +} + +/// Default model selection for an agent. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AgentModelSelection { + /// Provider id when the agent stores it separately or the adapter can + /// infer it safely. + pub provider_id: Option, + + /// Provider-local model id when known. + pub model_id: String, + + /// Full agent-specific model id. + /// + /// This is useful for agents such as OpenCode where the selected model may + /// be a provider-qualified route and the model id itself may contain `/`. + pub qualified_model_id: Option, +} + +impl AgentModelSelection { + /// Create a selection with only a model id. + pub fn model(model_id: impl Into) -> Self { + Self { + provider_id: None, + model_id: model_id.into(), + qualified_model_id: None, + } + } + + /// Create a selection with separate provider and model ids. + pub fn provider_model( + provider_id: impl Into, + model_id: impl Into, + ) -> Self { + Self { + provider_id: Some(provider_id.into()), + model_id: model_id.into(), + qualified_model_id: None, + } + } + + /// Create a selection where the agent persists one qualified model id. + pub fn qualified(qualified_model_id: impl Into) -> Self { + let qualified_model_id = qualified_model_id.into(); + Self { + provider_id: None, + model_id: qualified_model_id.clone(), + qualified_model_id: Some(qualified_model_id), + } + } +} + +/// Complete provider state loaded from an agent. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct AgentProviderState { + /// Providers currently visible in the agent config. + #[serde(default)] + pub providers: Vec, + + /// Primary/default model selection. + pub default_model: Option, + + /// Small/background model selection. + pub small_model: Option, +} + +impl AgentProviderState { + /// Validate state against an agent capability profile. + pub fn validate( + &self, + agent_id: &str, + capabilities: &AgentProviderCapabilities, + ) -> Result<()> { + if self.providers.len() > 1 { + capabilities.ensure_supports( + agent_id, + AgentProviderCapability::MultipleProviders, + )?; + } + + for provider in &self.providers { + clean_agent_provider_id(provider.id.clone())?; + match provider.source { + AgentProviderSource::ClosedSlot => {} + AgentProviderSource::BuiltIn => capabilities.ensure_supports( + agent_id, + AgentProviderCapability::BuiltInProviders, + )?, + AgentProviderSource::Custom => capabilities.ensure_supports( + agent_id, + AgentProviderCapability::CustomProviders, + )?, + } + + if let Some(capability) = + credential_capability(&provider.credential) + { + capabilities.ensure_supports(agent_id, capability)?; + } + } + + if let Some(selection) = &self.default_model { + capabilities.ensure_supports( + agent_id, + AgentProviderCapability::DefaultModel, + )?; + validate_selection(agent_id, capabilities, selection)?; + } + + if self.small_model.is_some() { + capabilities.ensure_supports( + agent_id, + AgentProviderCapability::SmallModel, + )?; + if let Some(selection) = &self.small_model { + validate_selection(agent_id, capabilities, selection)?; + } + } + + Ok(()) + } +} + +/// Adapter boundary implemented by agent-specific provider config handlers. +pub trait AgentProviderAdapter { + /// Stable agent id, for example `claude`, `opencode`, or `codex`. + fn agent_id(&self) -> &'static str; + + /// Provider-management capabilities for this agent. + fn capabilities(&self) -> AgentProviderCapabilities; + + /// Load provider state from the agent's backing config. + fn load_providers(&self) -> Result; + + /// Persist provider state to the agent's backing config. + fn save_providers(&self, state: &AgentProviderState) -> Result<()>; +} + +fn clean_agent_provider_id(id: String) -> Result { + let id = id.trim().to_string(); + if id.is_empty() { + Err(InferenceProviderError::EmptyAgentProviderId) + } else { + Ok(id) + } +} + +fn credential_capability( + credential: &AgentProviderCredential, +) -> Option { + match credential { + AgentProviderCredential::None => None, + AgentProviderCredential::EnvVar { .. } => { + Some(AgentProviderCapability::EnvVarCredentials) + } + AgentProviderCredential::AgentStore { .. } => { + Some(AgentProviderCapability::AgentCredentialStore) + } + AgentProviderCredential::Inline => { + Some(AgentProviderCapability::InlineApiKeyCredentials) + } + } +} + +fn validate_selection( + agent_id: &str, + capabilities: &AgentProviderCapabilities, + selection: &AgentModelSelection, +) -> Result<()> { + if selection.provider_id.is_some() + && !capabilities.supports(AgentProviderCapability::DefaultProvider) + { + capabilities.ensure_supports( + agent_id, + AgentProviderCapability::ProviderQualifiedModel, + )?; + } + + if selection.qualified_model_id.is_some() { + capabilities.ensure_supports( + agent_id, + AgentProviderCapability::ProviderQualifiedModel, + )?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn provider() -> InferenceProvider { + InferenceProvider { + id: "inventory-id".to_string(), + name: "OpenRouter".to_string(), + format: InferenceProviderFormat::OpenAiResponses, + api_base_url: "https://openrouter.ai/api/v1".to_string(), + models: vec![ + "openai/gpt-5.4".to_string(), + "anthropic/claude-sonnet-4-5".to_string(), + ], + } + } + + #[test] + fn closed_single_caps_do_not_support_multiple_providers() { + let caps = AgentProviderCapabilities::closed_single( + AgentProviderDefaultSupport::MODEL_ONLY, + AgentCredentialSupport::ENV_VAR, + ); + + assert!(caps.supports(AgentProviderCapability::DefaultModel)); + assert!(!caps.supports(AgentProviderCapability::CustomProviders)); + assert!(!caps.supports(AgentProviderCapability::MultipleProviders)); + } + + #[test] + fn opencode_caps_support_registry_and_qualified_models() { + let caps = AgentProviderCapabilities::registry( + AgentProviderDefaultSupport::QUALIFIED_MODELS, + AgentCredentialSupport::ENV_VAR_OR_AGENT_STORE, + BuiltInProviderSupport::OVERRIDABLE, + ); + + assert!(caps.supports(AgentProviderCapability::CustomProviders)); + assert!(caps.supports(AgentProviderCapability::MultipleProviders)); + assert!(caps.supports(AgentProviderCapability::ProviderQualifiedModel)); + assert!(!caps.supports(AgentProviderCapability::DefaultProvider)); + } + + #[test] + fn codex_caps_support_default_provider_and_registry() { + let caps = AgentProviderCapabilities::registry( + AgentProviderDefaultSupport::PROVIDER_AND_MODEL, + AgentCredentialSupport::ENV_VAR, + BuiltInProviderSupport::IMMUTABLE, + ); + + assert!(caps.supports(AgentProviderCapability::DefaultProvider)); + assert!(caps.supports(AgentProviderCapability::CustomProviders)); + assert!( + !caps.supports(AgentProviderCapability::OverrideBuiltInProviders) + ); + } + + #[test] + fn binding_from_inventory_keeps_inventory_id_separate() { + let binding = AgentProviderBinding::from_inventory( + "openrouter", + &provider(), + AgentProviderCredential::EnvVar { + name: "OPENROUTER_API_KEY".to_string(), + }, + AgentProviderSource::Custom, + ) + .unwrap(); + + assert_eq!(binding.id, "openrouter"); + assert_eq!(binding.source_provider_id.as_deref(), Some("inventory-id")); + assert_eq!(binding.models.len(), 2); + } + + #[test] + fn binding_rejects_empty_agent_provider_id() { + let err = AgentProviderBinding::from_inventory( + " ", + &provider(), + AgentProviderCredential::None, + AgentProviderSource::Custom, + ) + .unwrap_err(); + + assert!(matches!(err, InferenceProviderError::EmptyAgentProviderId)); + } + + #[test] + fn state_validation_rejects_unsupported_small_model() { + let caps = AgentProviderCapabilities::registry( + AgentProviderDefaultSupport::PROVIDER_AND_MODEL, + AgentCredentialSupport::ENV_VAR, + BuiltInProviderSupport::IMMUTABLE, + ); + let state = AgentProviderState { + providers: Vec::new(), + default_model: None, + small_model: Some(AgentModelSelection::model("gpt-5.4-mini")), + }; + + let err = state.validate("codex", &caps).unwrap_err(); + assert!(matches!( + err, + InferenceProviderError::UnsupportedAgentProviderCapability { .. } + )); + } + + #[test] + fn state_validation_checks_source_and_credentials() { + let caps = AgentProviderCapabilities::closed_single( + AgentProviderDefaultSupport::MODEL_ONLY, + AgentCredentialSupport::ENV_VAR, + ); + let provider = AgentProviderBinding::from_inventory( + "primary", + &provider(), + AgentProviderCredential::Inline, + AgentProviderSource::Custom, + ) + .unwrap(); + let state = AgentProviderState { + providers: vec![provider], + default_model: None, + small_model: None, + }; + + let err = state.validate("claude", &caps).unwrap_err(); + assert!(matches!( + err, + InferenceProviderError::UnsupportedAgentProviderCapability { .. } + )); + } + + #[test] + fn state_validation_accepts_opencode_qualified_selection() { + let caps = AgentProviderCapabilities::registry( + AgentProviderDefaultSupport::QUALIFIED_MODELS, + AgentCredentialSupport::ENV_VAR_OR_AGENT_STORE, + BuiltInProviderSupport::OVERRIDABLE, + ); + let state = AgentProviderState { + providers: Vec::new(), + default_model: Some(AgentModelSelection { + provider_id: Some("anthropic".to_string()), + model_id: "claude-sonnet-4-5".to_string(), + qualified_model_id: Some( + "anthropic/claude-sonnet-4-5".to_string(), + ), + }), + small_model: None, + }; + + state.validate("opencode", &caps).unwrap(); + } + + #[test] + fn state_validation_rejects_multiple_closed_slot_bindings() { + let caps = AgentProviderCapabilities::closed_single( + AgentProviderDefaultSupport::MODEL_ONLY, + AgentCredentialSupport::ENV_VAR, + ); + let first = AgentProviderBinding::from_inventory( + "primary", + &provider(), + AgentProviderCredential::EnvVar { + name: "ANTHROPIC_API_KEY".to_string(), + }, + AgentProviderSource::ClosedSlot, + ) + .unwrap(); + let second = AgentProviderBinding { + id: "secondary".to_string(), + ..first.clone() + }; + let state = AgentProviderState { + providers: vec![first, second], + default_model: None, + small_model: None, + }; + + let err = state.validate("claude", &caps).unwrap_err(); + assert!(matches!( + err, + InferenceProviderError::UnsupportedAgentProviderCapability { .. } + )); + } +} diff --git a/crates/inference/src/error.rs b/crates/inference/src/error.rs index cb9e7188..1b6c63ac 100644 --- a/crates/inference/src/error.rs +++ b/crates/inference/src/error.rs @@ -47,6 +47,19 @@ pub enum InferenceProviderError { #[error("provider not found: {0}")] NotFound(String), + /// Agent-local provider IDs must not be empty. + #[error("agent provider id cannot be empty")] + EmptyAgentProviderId, + + /// Agent does not support the requested provider-management capability. + #[error("{agent_id} does not support {capability}")] + UnsupportedAgentProviderCapability { + /// Stable agent id. + agent_id: String, + /// Unsupported capability label. + capability: String, + }, + /// Tauri app data directory could not be resolved. #[error("failed to resolve Tauri app data directory: {0}")] AppDataDir(String), diff --git a/crates/inference/src/lib.rs b/crates/inference/src/lib.rs index 7e578fdb..f5822099 100644 --- a/crates/inference/src/lib.rs +++ b/crates/inference/src/lib.rs @@ -4,11 +4,19 @@ //! app data directory. API keys are stored separately via the platform-native //! keyring. +pub mod agent; pub mod credentials; pub mod error; pub mod model; pub mod store; +pub use agent::{ + AgentCredentialSupport, AgentModelSelection, AgentProviderAdapter, + AgentProviderBinding, AgentProviderCapabilities, AgentProviderCapability, + AgentProviderCredential, AgentProviderDefaultSupport, AgentProviderMode, + AgentProviderModel, AgentProviderSource, AgentProviderState, + BuiltInProviderSupport, +}; pub use credentials::{CredentialStore, NativeCredentialStore}; pub use error::{InferenceProviderError, Result}; pub use model::{ From b3927f659deb9b04c7a5bb60a87b19c7e786ef67 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sun, 26 Apr 2026 14:03:55 +0800 Subject: [PATCH 17/62] feat(inference): add opencode provider management --- Cargo.lock | 22 ++ Cargo.toml | 6 + crates/api/src/error.rs | 14 +- crates/inference/Cargo.toml | 3 + crates/inference/src/agent.rs | 12 +- crates/inference/src/error.rs | 22 ++ crates/inference/src/lib.rs | 2 + crates/inference/src/opencode/files.rs | 135 ++++++++++++ crates/inference/src/opencode/mapping.rs | 241 ++++++++++++++++++++++ crates/inference/src/opencode/mod.rs | 201 ++++++++++++++++++ crates/inference/src/opencode/schema.rs | 54 +++++ crates/inference/src/opencode/tests.rs | 249 +++++++++++++++++++++++ crates/json/Cargo.toml | 13 ++ crates/json/src/lib.rs | 192 +++++++++++++++++ 14 files changed, 1159 insertions(+), 7 deletions(-) create mode 100644 crates/inference/src/opencode/files.rs create mode 100644 crates/inference/src/opencode/mapping.rs create mode 100644 crates/inference/src/opencode/mod.rs create mode 100644 crates/inference/src/opencode/schema.rs create mode 100644 crates/inference/src/opencode/tests.rs create mode 100644 crates/json/Cargo.toml create mode 100644 crates/json/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 27f4ace7..bdb3db12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,8 @@ dependencies = [ name = "aghub-inference" version = "0.1.2" dependencies = [ + "aghub-json", + "dirs", "keyring", "serde", "serde_json", @@ -139,6 +141,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "aghub-json" +version = "0.1.2" +dependencies = [ + "jsonc-parser", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "aghub-markdown" version = "0.1.2" @@ -3853,6 +3865,16 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "jsonc-parser" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2e5dea04f54739ca5694ee06e4448ffda065a55b3427d2b131bd5ea697ea2c" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "jsonptr" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index d956c798..bf6ec85d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/skills-sh", "crates/skill", "crates/git", + "crates/json", "crates/markdown", "crates/inference", "crates/desktop/src-tauri", @@ -24,6 +25,11 @@ repository = "https://github.com/akarachen/aghub" # Core dependencies serde = { version = "1.0", features = [ "derive" ] } serde_json = { version = "1.0", features = [ "preserve_order" ] } +jsonc-parser = { version = "0.32.3", features = [ + "cst", + "serde", + "serde_json", +] } thiserror = "2.0" dirs = "6.0" ts-rs = "12.0.1" diff --git a/crates/api/src/error.rs b/crates/api/src/error.rs index 9f1bdcfe..455c31cc 100644 --- a/crates/api/src/error.rs +++ b/crates/api/src/error.rs @@ -94,11 +94,15 @@ impl From for ApiError { | InferenceProviderError::InvalidFormat(_) | InferenceProviderError::UnsupportedAgentProviderCapability { .. - } => ApiError::new( - Status::BadRequest, - e.to_string(), - "INVALID_PARAM", - ), + } + | InferenceProviderError::InvalidAgentProviderConfig { .. } + | InferenceProviderError::InvalidAgentCredentialStore { .. } => { + ApiError::new( + Status::BadRequest, + e.to_string(), + "INVALID_PARAM", + ) + } InferenceProviderError::AlreadyExists(_) | InferenceProviderError::ModelAlreadyExists(_) => ApiError::new( Status::Conflict, diff --git a/crates/inference/Cargo.toml b/crates/inference/Cargo.toml index 2c67b951..dfcc3e49 100644 --- a/crates/inference/Cargo.toml +++ b/crates/inference/Cargo.toml @@ -7,8 +7,11 @@ license.workspace = true description = "Inference provider configuration storage for aghub" [dependencies] +aghub-json = { path = "../json" } serde = { workspace = true } +serde_json = { workspace = true } thiserror = { workspace = true } +dirs = { workspace = true } keyring = { version = "3", features = [ "apple-native", "windows-native", diff --git a/crates/inference/src/agent.rs b/crates/inference/src/agent.rs index 4f4e59a5..4a2dfda1 100644 --- a/crates/inference/src/agent.rs +++ b/crates/inference/src/agent.rs @@ -332,6 +332,9 @@ pub enum AgentProviderSource { /// Custom provider from user configuration. Custom, + + /// Provider discovered only from the agent's credential store. + StoredCredential, } /// Model entry attached to an agent provider. @@ -382,7 +385,7 @@ pub struct AgentProviderBinding { pub name: String, /// Request/response wire format. - pub format: InferenceProviderFormat, + pub format: Option, /// API base URL, if the agent exposes one. pub api_base_url: Option, @@ -411,7 +414,7 @@ impl AgentProviderBinding { id, source_provider_id: Some(provider.id.clone()), name: provider.name.clone(), - format: provider.format, + format: Some(provider.format), api_base_url: Some(provider.api_base_url.clone()), credential, models: provider @@ -515,6 +518,11 @@ impl AgentProviderState { agent_id, AgentProviderCapability::CustomProviders, )?, + AgentProviderSource::StoredCredential => capabilities + .ensure_supports( + agent_id, + AgentProviderCapability::AgentCredentialStore, + )?, } if let Some(capability) = diff --git a/crates/inference/src/error.rs b/crates/inference/src/error.rs index 1b6c63ac..2da1359f 100644 --- a/crates/inference/src/error.rs +++ b/crates/inference/src/error.rs @@ -60,6 +60,28 @@ pub enum InferenceProviderError { capability: String, }, + /// Agent provider config could not be parsed. + #[error("invalid {agent_id} provider config at {path}: {message}")] + InvalidAgentProviderConfig { + /// Stable agent id. + agent_id: String, + /// Config path. + path: String, + /// Parse or validation message. + message: String, + }, + + /// Agent credential store could not be parsed. + #[error("invalid {agent_id} credential store at {path}: {message}")] + InvalidAgentCredentialStore { + /// Stable agent id. + agent_id: String, + /// Credential store path. + path: String, + /// Parse or validation message. + message: String, + }, + /// Tauri app data directory could not be resolved. #[error("failed to resolve Tauri app data directory: {0}")] AppDataDir(String), diff --git a/crates/inference/src/lib.rs b/crates/inference/src/lib.rs index f5822099..49d9570e 100644 --- a/crates/inference/src/lib.rs +++ b/crates/inference/src/lib.rs @@ -8,6 +8,7 @@ pub mod agent; pub mod credentials; pub mod error; pub mod model; +pub mod opencode; pub mod store; pub use agent::{ @@ -23,6 +24,7 @@ pub use model::{ CreateInferenceProvider, InferenceProvider, InferenceProviderFormat, UpdateInferenceProvider, }; +pub use opencode::OpenCodeProviderAdapter; pub use store::{ InferenceProviderRepository, InferenceProviderStore, INFERENCE_PROVIDERS_FILE, diff --git a/crates/inference/src/opencode/files.rs b/crates/inference/src/opencode/files.rs new file mode 100644 index 00000000..e496f328 --- /dev/null +++ b/crates/inference/src/opencode/files.rs @@ -0,0 +1,135 @@ +//! File I/O for OpenCode provider config and auth storage. + +use std::collections::BTreeMap; +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use aghub_json::{parse_jsonc_opt, patch_jsonc_object}; +use serde_json::Value; + +use super::schema::OpenCodeConfig; +use super::AGENT_ID; +use crate::error::{InferenceProviderError, Result}; + +pub(super) fn read_config(path: &Path) -> Result { + let content = match fs::read_to_string(path) { + Ok(content) => content, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return Ok(OpenCodeConfig::default()); + } + Err(error) => return Err(error.into()), + }; + + parse_jsonc_opt(&content) + .map(|config| config.unwrap_or_default()) + .map_err(|error| invalid_config(path, error.to_string())) +} + +pub(super) fn write_config(path: &Path, config: OpenCodeConfig) -> Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + let existing = existing_content(path)?; + let content = patch_jsonc_object(existing.as_deref(), &config) + .map_err(|error| invalid_config(path, error.to_string()))?; + fs::write(path, content)?; + Ok(()) +} + +pub(super) fn read_auth_values(path: &Path) -> Result> { + let content = match fs::read_to_string(path) { + Ok(content) => content, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return Ok(BTreeMap::new()); + } + Err(error) => return Err(error.into()), + }; + + parse_jsonc_opt(&content) + .map(|auth| auth.unwrap_or_default()) + .map_err(|error| invalid_auth(path, error.to_string())) +} + +pub(super) fn write_auth_values( + path: &Path, + auth: &BTreeMap, +) -> Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + let existing = existing_content(path)?; + let content = patch_jsonc_object(existing.as_deref(), auth) + .map_err(|error| invalid_auth(path, error.to_string()))?; + + let mut options = fs::OpenOptions::new(); + options.create(true).truncate(true).write(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let mut file = options.open(path)?; + file.write_all(content.as_bytes())?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(0o600))?; + } + Ok(()) +} + +pub(super) fn default_global_config_path() -> Result { + let config_home = std::env::var_os("XDG_CONFIG_HOME") + .map(PathBuf::from) + .or_else(|| dirs::home_dir().map(|home| home.join(".config"))) + .ok_or_else(home_dir_error)?; + Ok(config_home.join("opencode/opencode.json")) +} + +pub(super) fn default_auth_path() -> Result { + let data_home = std::env::var_os("XDG_DATA_HOME") + .map(PathBuf::from) + .or_else(|| dirs::home_dir().map(|home| home.join(".local/share"))) + .ok_or_else(home_dir_error)?; + Ok(data_home.join("opencode/auth.json")) +} + +fn existing_content(path: &Path) -> Result> { + match fs::read_to_string(path) { + Ok(content) => Ok(Some(content)), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(error) => Err(error.into()), + } +} + +fn home_dir_error() -> InferenceProviderError { + InferenceProviderError::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + "home directory not found", + )) +} + +fn invalid_config( + path: &Path, + message: impl Into, +) -> InferenceProviderError { + InferenceProviderError::InvalidAgentProviderConfig { + agent_id: AGENT_ID.to_string(), + path: path.display().to_string(), + message: message.into(), + } +} + +fn invalid_auth( + path: &Path, + message: impl Into, +) -> InferenceProviderError { + InferenceProviderError::InvalidAgentCredentialStore { + agent_id: AGENT_ID.to_string(), + path: path.display().to_string(), + message: message.into(), + } +} diff --git a/crates/inference/src/opencode/mapping.rs b/crates/inference/src/opencode/mapping.rs new file mode 100644 index 00000000..6d6ada3d --- /dev/null +++ b/crates/inference/src/opencode/mapping.rs @@ -0,0 +1,241 @@ +//! Mapping between OpenCode provider files and normalized agent state. + +use std::collections::BTreeMap; + +use serde_json::Value; + +use super::schema::OpenCodeProviderConfig; +use crate::agent::{ + AgentModelSelection, AgentProviderBinding, AgentProviderCredential, + AgentProviderModel, AgentProviderSource, +}; +use crate::error::{InferenceProviderError, Result}; +use crate::model::InferenceProviderFormat; + +const OPENAI_COMPATIBLE_NPM: &str = "@ai-sdk/openai-compatible"; +const OPENAI_NPM: &str = "@ai-sdk/openai"; +const ANTHROPIC_NPM: &str = "@ai-sdk/anthropic"; + +pub(super) fn binding_from_config( + provider_id: &str, + provider: &OpenCodeProviderConfig, + auth: &BTreeMap, +) -> Result { + Ok(AgentProviderBinding { + id: clean_provider_id(provider_id)?, + source_provider_id: None, + name: provider + .name + .clone() + .unwrap_or_else(|| provider_id.to_string()), + format: provider.npm.as_deref().and_then(format_from_npm), + api_base_url: provider + .options + .get("baseURL") + .and_then(Value::as_str) + .or_else(|| { + provider.options.get("endpoint").and_then(Value::as_str) + }) + .map(ToString::to_string), + credential: credential_from_config(provider_id, provider, auth), + models: provider + .models + .iter() + .map(|(model_id, model)| AgentProviderModel { + id: model_id.clone(), + name: model.name.clone(), + }) + .collect(), + source: AgentProviderSource::Custom, + }) +} + +pub(super) fn binding_from_auth( + provider_id: &str, + entry: &Value, +) -> AgentProviderBinding { + let credential = match entry.get("type").and_then(Value::as_str) { + Some("api") | Some("oauth") | Some("wellknown") => { + AgentProviderCredential::AgentStore { + id: Some(provider_id.to_string()), + } + } + _ => AgentProviderCredential::None, + }; + + AgentProviderBinding { + id: provider_id.to_string(), + source_provider_id: None, + name: provider_id.to_string(), + format: None, + api_base_url: None, + credential, + models: Vec::new(), + source: AgentProviderSource::StoredCredential, + } +} + +pub(super) fn provider_config_from_binding( + binding: &AgentProviderBinding, + existing: Option<&OpenCodeProviderConfig>, +) -> OpenCodeProviderConfig { + let mut provider = existing.cloned().unwrap_or_default(); + if let Some(format) = binding.format { + provider.npm = Some(npm_from_format(format).to_string()); + } + + provider.name = Some(binding.name.clone()); + + if let Some(api_base_url) = &binding.api_base_url { + provider + .options + .insert("baseURL".to_string(), Value::String(api_base_url.clone())); + } else { + provider.options.remove("baseURL"); + } + + match &binding.credential { + AgentProviderCredential::EnvVar { name } => { + provider.options.insert( + "apiKey".to_string(), + Value::String(format!("{{env:{name}}}")), + ); + } + AgentProviderCredential::AgentStore { .. } => { + provider.options.remove("apiKey"); + } + AgentProviderCredential::None => { + provider.options.remove("apiKey"); + } + AgentProviderCredential::Inline => {} + } + + let original_models = provider.models; + provider.models = binding + .models + .iter() + .map(|model| { + let mut next = + original_models.get(&model.id).cloned().unwrap_or_default(); + next.name = model.name.clone(); + (model.id.clone(), next) + }) + .collect(); + + provider +} + +pub(super) fn parse_opencode_model_selection( + value: &str, +) -> AgentModelSelection { + let Some((provider_id, model_id)) = value.split_once('/') else { + return AgentModelSelection::qualified(value.to_string()); + }; + + AgentModelSelection { + provider_id: Some(provider_id.to_string()), + model_id: model_id.to_string(), + qualified_model_id: Some(value.to_string()), + } +} + +pub(super) fn format_selection(selection: &AgentModelSelection) -> String { + if let Some(qualified_model_id) = &selection.qualified_model_id { + return qualified_model_id.clone(); + } + if let Some(provider_id) = &selection.provider_id { + return format!("{provider_id}/{}", selection.model_id); + } + selection.model_id.clone() +} + +pub(super) fn provider_id_from_name(name: &str) -> String { + let mut out = String::new(); + let mut previous_was_dash = false; + + for ch in name.chars().flat_map(char::to_lowercase) { + if ch.is_ascii_alphanumeric() { + out.push(ch); + previous_was_dash = false; + } else if !previous_was_dash && !out.is_empty() { + out.push('-'); + previous_was_dash = true; + } + } + + while out.ends_with('-') { + out.pop(); + } + + if out.is_empty() { + "provider".to_string() + } else { + out + } +} + +pub(super) fn clean_provider_id(provider_id: &str) -> Result { + let provider_id = provider_id.trim().trim_end_matches('/').to_string(); + if provider_id.is_empty() { + Err(InferenceProviderError::EmptyAgentProviderId) + } else { + Ok(provider_id) + } +} + +pub(super) fn ensure_api_key(api_key: &str) -> Result<()> { + if api_key.trim().is_empty() { + Err(InferenceProviderError::EmptyApiKey) + } else { + Ok(()) + } +} + +fn credential_from_config( + provider_id: &str, + provider: &OpenCodeProviderConfig, + auth: &BTreeMap, +) -> AgentProviderCredential { + if auth.contains_key(provider_id) { + return AgentProviderCredential::AgentStore { + id: Some(provider_id.to_string()), + }; + } + + let Some(api_key) = provider.options.get("apiKey").and_then(Value::as_str) + else { + return AgentProviderCredential::None; + }; + + parse_env_var_ref(api_key) + .map(|name| AgentProviderCredential::EnvVar { name }) + .unwrap_or(AgentProviderCredential::Inline) +} + +fn format_from_npm(npm: &str) -> Option { + match npm { + OPENAI_COMPATIBLE_NPM => { + Some(InferenceProviderFormat::OpenAiCompletions) + } + OPENAI_NPM => Some(InferenceProviderFormat::OpenAiResponses), + ANTHROPIC_NPM => Some(InferenceProviderFormat::Anthropic), + _ => None, + } +} + +fn npm_from_format(format: InferenceProviderFormat) -> &'static str { + match format { + InferenceProviderFormat::Anthropic => ANTHROPIC_NPM, + InferenceProviderFormat::OpenAiCompletions => OPENAI_COMPATIBLE_NPM, + InferenceProviderFormat::OpenAiResponses => OPENAI_NPM, + } +} + +fn parse_env_var_ref(value: &str) -> Option { + let name = value.strip_prefix("{env:")?.strip_suffix('}')?.trim(); + if name.is_empty() { + None + } else { + Some(name.to_string()) + } +} diff --git a/crates/inference/src/opencode/mod.rs b/crates/inference/src/opencode/mod.rs new file mode 100644 index 00000000..f5f0eb0f --- /dev/null +++ b/crates/inference/src/opencode/mod.rs @@ -0,0 +1,201 @@ +//! OpenCode provider configuration adapter. +//! +//! OpenCode splits provider configuration and credentials. Provider metadata +//! lives in `opencode.json`, while credentials written by `/connect` live in +//! `~/.local/share/opencode/auth.json`. + +mod files; +mod mapping; +mod schema; + +#[cfg(test)] +mod tests; + +use std::collections::HashSet; +use std::path::{Path, PathBuf}; + +use serde_json::json; + +use crate::agent::{ + AgentCredentialSupport, AgentProviderAdapter, AgentProviderBinding, + AgentProviderCapabilities, AgentProviderCredential, + AgentProviderDefaultSupport, AgentProviderSource, AgentProviderState, + BuiltInProviderSupport, +}; +use crate::error::Result; +use crate::model::InferenceProvider; + +pub(super) const AGENT_ID: &str = "opencode"; + +/// Provider adapter for OpenCode. +#[derive(Debug, Clone)] +pub struct OpenCodeProviderAdapter { + config_path: PathBuf, + auth_path: PathBuf, +} + +impl OpenCodeProviderAdapter { + /// Create an adapter with explicit config and auth paths. + pub fn new( + config_path: impl Into, + auth_path: impl Into, + ) -> Self { + Self { + config_path: config_path.into(), + auth_path: auth_path.into(), + } + } + + /// Create an adapter for the global OpenCode config. + pub fn global() -> Result { + Ok(Self::new( + files::default_global_config_path()?, + files::default_auth_path()?, + )) + } + + /// Create an adapter for a project-local `opencode.json`. + pub fn for_project(project_root: impl AsRef) -> Result { + Ok(Self::new( + project_root.as_ref().join("opencode.json"), + files::default_auth_path()?, + )) + } + + /// Path to the OpenCode config file this adapter manages. + pub fn config_path(&self) -> &Path { + &self.config_path + } + + /// Path to the OpenCode auth file this adapter manages. + pub fn auth_path(&self) -> &Path { + &self.auth_path + } + + /// Add or replace an aghub provider in OpenCode. + /// + /// This writes the provider definition to `opencode.json` and stores the + /// API key in `auth.json`, matching OpenCode's `/connect` behavior. + pub fn add_provider( + &self, + provider_id: &str, + provider: &InferenceProvider, + api_key: &str, + ) -> Result { + mapping::ensure_api_key(api_key)?; + let provider_id = mapping::clean_provider_id(provider_id)?; + let credential = AgentProviderCredential::AgentStore { + id: Some(provider_id.clone()), + }; + let binding = AgentProviderBinding::from_inventory( + provider_id.clone(), + provider, + credential, + AgentProviderSource::Custom, + )?; + + let mut state = self.load_providers()?; + state.providers.retain(|item| item.id != provider_id); + state.providers.push(binding.clone()); + self.save_providers(&state)?; + self.set_api_auth(&provider_id, api_key)?; + + Ok(binding) + } + + /// Add a provider using a slug derived from its display name. + pub fn add_inventory_provider( + &self, + provider: &InferenceProvider, + api_key: &str, + ) -> Result { + let provider_id = mapping::provider_id_from_name(&provider.name); + self.add_provider(&provider_id, provider, api_key) + } + + fn set_api_auth(&self, provider_id: &str, api_key: &str) -> Result<()> { + let mut auth = files::read_auth_values(&self.auth_path)?; + auth.insert( + provider_id.to_string(), + json!({ + "type": "api", + "key": api_key, + }), + ); + files::write_auth_values(&self.auth_path, &auth) + } +} + +impl AgentProviderAdapter for OpenCodeProviderAdapter { + fn agent_id(&self) -> &'static str { + AGENT_ID + } + + fn capabilities(&self) -> AgentProviderCapabilities { + AgentProviderCapabilities::registry( + AgentProviderDefaultSupport::QUALIFIED_MODELS, + AgentCredentialSupport::ENV_VAR_OR_AGENT_STORE, + BuiltInProviderSupport::OVERRIDABLE, + ) + } + + fn load_providers(&self) -> Result { + let config = files::read_config(&self.config_path)?; + let auth = files::read_auth_values(&self.auth_path)?; + let mut seen = HashSet::new(); + let mut providers = Vec::new(); + + for (provider_id, provider) in &config.provider { + seen.insert(provider_id.clone()); + providers.push(mapping::binding_from_config( + provider_id, + provider, + &auth, + )?); + } + + for (provider_id, entry) in &auth { + if seen.contains(provider_id) { + continue; + } + providers.push(mapping::binding_from_auth(provider_id, entry)); + } + + Ok(AgentProviderState { + providers, + default_model: config + .model + .as_deref() + .map(mapping::parse_opencode_model_selection), + small_model: config + .small_model + .as_deref() + .map(mapping::parse_opencode_model_selection), + }) + } + + fn save_providers(&self, state: &AgentProviderState) -> Result<()> { + state.validate(AGENT_ID, &self.capabilities())?; + + let mut config = files::read_config(&self.config_path)?; + let original = config.provider.clone(); + let mut providers = std::collections::BTreeMap::new(); + + for binding in &state.providers { + if binding.source == AgentProviderSource::StoredCredential { + continue; + } + let existing = original.get(&binding.id); + let provider = + mapping::provider_config_from_binding(binding, existing); + providers.insert(binding.id.clone(), provider); + } + + config.provider = providers; + config.model = + state.default_model.as_ref().map(mapping::format_selection); + config.small_model = + state.small_model.as_ref().map(mapping::format_selection); + files::write_config(&self.config_path, config) + } +} diff --git a/crates/inference/src/opencode/schema.rs b/crates/inference/src/opencode/schema.rs new file mode 100644 index 00000000..095b2b79 --- /dev/null +++ b/crates/inference/src/opencode/schema.rs @@ -0,0 +1,54 @@ +//! OpenCode JSON schema fragments used by the provider adapter. + +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub(super) struct OpenCodeConfig { + #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] + pub schema: Option, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub provider: BTreeMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub small_model: Option, + #[serde(flatten)] + pub extra: Map, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub(super) struct OpenCodeProviderConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub npm: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(default, skip_serializing_if = "is_json_map_empty")] + pub options: Map, + #[serde(default, skip_serializing_if = "is_model_map_empty")] + pub models: BTreeMap, + #[serde(flatten)] + pub extra: Map, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub(super) struct OpenCodeModelConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(default, skip_serializing_if = "is_json_map_empty")] + pub options: Map, + #[serde(flatten)] + pub extra: Map, +} + +fn is_json_map_empty(map: &Map) -> bool { + map.is_empty() +} + +fn is_model_map_empty(map: &BTreeMap) -> bool { + map.is_empty() +} diff --git a/crates/inference/src/opencode/tests.rs b/crates/inference/src/opencode/tests.rs new file mode 100644 index 00000000..88b87682 --- /dev/null +++ b/crates/inference/src/opencode/tests.rs @@ -0,0 +1,249 @@ +use std::fs; + +use serde_json::Value; + +use super::*; +use crate::agent::{AgentProviderAdapter, AgentProviderCredential}; +use crate::model::{InferenceProvider, InferenceProviderFormat}; + +fn adapter(temp: &tempfile::TempDir) -> OpenCodeProviderAdapter { + OpenCodeProviderAdapter::new( + temp.path().join("opencode.json"), + temp.path().join("auth.json"), + ) +} + +fn provider() -> InferenceProvider { + InferenceProvider { + id: "inventory-id".to_string(), + name: "OpenRouter".to_string(), + format: InferenceProviderFormat::OpenAiCompletions, + api_base_url: "https://openrouter.ai/api/v1".to_string(), + models: vec!["anthropic/claude-sonnet-4-5".to_string()], + } +} + +#[test] +fn load_scans_config_and_auth_store() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#"{ + "$schema": "https://opencode.ai/config.json", + "model": "myprovider/my-model", + "small_model": "myprovider/small-model", + "provider": { + "myprovider": { + "npm": "@ai-sdk/openai-compatible", + "name": "My Provider", + "options": { + "baseURL": "https://api.example.com/v1" + }, + "models": { + "my-model": { "name": "My Model" } + } + } + } + }"#, + ) + .unwrap(); + fs::write( + adapter.auth_path(), + r#"{ + "myprovider": { "type": "api", "key": "secret" }, + "auth-only": { "type": "oauth", "access": "a", "refresh": "r", "expires": 1 } + }"#, + ) + .unwrap(); + + let state = adapter.load_providers().unwrap(); + + assert_eq!(state.providers.len(), 2); + let configured = state + .providers + .iter() + .find(|provider| provider.id == "myprovider") + .unwrap(); + assert_eq!(configured.name, "My Provider"); + assert_eq!( + configured.format, + Some(InferenceProviderFormat::OpenAiCompletions) + ); + assert_eq!( + configured.credential, + AgentProviderCredential::AgentStore { + id: Some("myprovider".to_string()) + } + ); + assert_eq!(configured.models[0].name.as_deref(), Some("My Model")); + + let auth_only = state + .providers + .iter() + .find(|provider| provider.id == "auth-only") + .unwrap(); + assert_eq!(auth_only.source, AgentProviderSource::StoredCredential); + assert_eq!(auth_only.format, None); + assert_eq!( + state.default_model.unwrap().qualified_model_id.as_deref(), + Some("myprovider/my-model") + ); +} + +#[test] +fn load_detects_env_api_key_reference() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#"{ + "provider": { + "env-provider": { + "options": { + "apiKey": "{env:OPENROUTER_API_KEY}" + } + } + } + }"#, + ) + .unwrap(); + + let state = adapter.load_providers().unwrap(); + + assert_eq!( + state.providers[0].credential, + AgentProviderCredential::EnvVar { + name: "OPENROUTER_API_KEY".to_string() + } + ); +} + +#[test] +fn add_provider_writes_config_and_auth_json() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + + let binding = adapter + .add_provider("openrouter", &provider(), "sk-test") + .unwrap(); + + assert_eq!(binding.id, "openrouter"); + let config: Value = serde_json::from_str( + &fs::read_to_string(adapter.config_path()).unwrap(), + ) + .unwrap(); + assert_eq!( + config["provider"]["openrouter"]["npm"], + "@ai-sdk/openai-compatible" + ); + assert_eq!( + config["provider"]["openrouter"]["options"]["baseURL"], + "https://openrouter.ai/api/v1" + ); + assert!(config["provider"]["openrouter"]["options"] + .get("apiKey") + .is_none()); + + let auth: Value = + serde_json::from_str(&fs::read_to_string(adapter.auth_path()).unwrap()) + .unwrap(); + assert_eq!(auth["openrouter"]["type"], "api"); + assert_eq!(auth["openrouter"]["key"], "sk-test"); +} + +#[test] +fn add_provider_preserves_jsonc_comments() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#"{ + // user-managed defaults + "model": "existing/model", + "provider": { + "existing": { + // keep endpoint note + "npm": "@ai-sdk/openai-compatible" + } + } + }"#, + ) + .unwrap(); + fs::write( + adapter.auth_path(), + r#"{ + // logged in from opencode + "existing": { "type": "api", "key": "old" } + }"#, + ) + .unwrap(); + + adapter + .add_provider("openrouter", &provider(), "sk-test") + .unwrap(); + + let config_content = fs::read_to_string(adapter.config_path()).unwrap(); + assert!(config_content.contains("// user-managed defaults")); + assert!(config_content.contains("// keep endpoint note")); + assert!(!config_content.contains(r#""apiKey""#)); + let config: Option = + aghub_json::parse_jsonc_opt(&config_content).unwrap(); + assert_eq!( + config.unwrap()["provider"]["openrouter"]["options"]["baseURL"], + "https://openrouter.ai/api/v1" + ); + + let auth_content = fs::read_to_string(adapter.auth_path()).unwrap(); + assert!(auth_content.contains("// logged in from opencode")); + let auth: Option = + aghub_json::parse_jsonc_opt(&auth_content).unwrap(); + assert_eq!(auth.unwrap()["openrouter"]["key"], "sk-test"); +} + +#[test] +fn save_preserves_auth_only_entries_without_configuring_them() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.auth_path(), + r#"{ "github-copilot": { "type": "oauth", "access": "a", "refresh": "r", "expires": 1 } }"#, + ) + .unwrap(); + + let state = adapter.load_providers().unwrap(); + adapter.save_providers(&state).unwrap(); + + let config: Value = serde_json::from_str( + &fs::read_to_string(adapter.config_path()).unwrap(), + ) + .unwrap(); + assert!(config.get("provider").is_none()); +} + +#[test] +fn jsonc_comments_are_accepted() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#"{ + // keep provider local to this test + "provider": { + "local": { + /* OpenAI-compatible endpoint */ + "npm": "@ai-sdk/openai-compatible" + } + } + }"#, + ) + .unwrap(); + + let state = adapter.load_providers().unwrap(); + + assert_eq!(state.providers[0].id, "local"); + assert_eq!( + state.providers[0].format, + Some(InferenceProviderFormat::OpenAiCompletions) + ); +} diff --git a/crates/json/Cargo.toml b/crates/json/Cargo.toml new file mode 100644 index 00000000..04c830bd --- /dev/null +++ b/crates/json/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "aghub-json" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +description = "JSON and JSONC editing helpers for aghub" + +[dependencies] +jsonc-parser = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/json/src/lib.rs b/crates/json/src/lib.rs new file mode 100644 index 00000000..cf23012d --- /dev/null +++ b/crates/json/src/lib.rs @@ -0,0 +1,192 @@ +//! JSON and JSONC parsing/editing helpers. +//! +//! The editing helpers use `jsonc-parser`'s CST API so callers can update +//! managed fields while preserving comments and formatting around untouched +//! fields. + +use std::collections::HashSet; + +use jsonc_parser::cst::{CstInputValue, CstObject, CstRootNode}; +use jsonc_parser::{parse_to_serde_value, ParseOptions}; +use serde::{de::DeserializeOwned, Serialize}; +use serde_json::{Map, Value}; +use thiserror::Error; + +/// Errors produced by JSON/JSONC helpers. +#[derive(Debug, Error)] +pub enum JsonError { + /// JSONC parsing failed. + #[error("JSONC parse error: {0}")] + Parse(#[from] jsonc_parser::errors::ParseError), + + /// Serialization into a JSON value failed. + #[error("JSON serialization error: {0}")] + Serialize(#[from] serde_json::Error), + + /// The root value must be an object for object patching. + #[error("expected JSON object")] + ExpectedObject, +} + +/// Parse JSONC into an optional serde value. +/// +/// Empty or whitespace-only input is returned as `None`, matching +/// `jsonc-parser`'s empty-input handling. +pub fn parse_jsonc_opt( + content: &str, +) -> Result, JsonError> { + Ok(parse_to_serde_value::>( + content, + &jsonc_parse_options(), + )?) +} + +/// Patch the root JSON object without reformatting the whole document. +/// +/// Existing object properties are recursively patched. Properties absent from +/// `value` are removed; properties present in `value` are updated or appended. +pub fn patch_jsonc_object( + content: Option<&str>, + value: &T, +) -> Result { + let value = serde_json::to_value(value)?; + let Value::Object(map) = value else { + return Err(JsonError::ExpectedObject); + }; + + let is_new_document = content.map(str::trim).unwrap_or("").is_empty(); + let root = + CstRootNode::parse(content.unwrap_or(""), &jsonc_parse_options())?; + let root_object = match root.object_value() { + Some(object) => object, + None if is_new_document => root.object_value_or_set(), + None => return Err(JsonError::ExpectedObject), + }; + patch_object(&root_object, &map); + + let mut output = root.to_string(); + if is_new_document && !output.ends_with('\n') { + output.push('\n'); + } + Ok(output) +} + +fn jsonc_parse_options() -> ParseOptions { + ParseOptions { + allow_comments: true, + allow_loose_object_property_names: false, + allow_trailing_commas: true, + allow_missing_commas: false, + allow_single_quoted_strings: false, + allow_hexadecimal_numbers: false, + allow_unary_plus_numbers: false, + } +} + +fn patch_object(object: &CstObject, desired: &Map) { + let desired_keys: HashSet<&str> = + desired.keys().map(String::as_str).collect(); + for prop in object.properties() { + let Some(name) = prop.name().and_then(|name| name.decoded_value().ok()) + else { + continue; + }; + if !desired_keys.contains(name.as_str()) { + prop.remove(); + } + } + + for (name, value) in desired { + match object.get(name) { + Some(prop) => { + if let Value::Object(map) = value { + if let Some(existing) = prop.object_value() { + patch_object(&existing, map); + continue; + } + } + if prop.to_serde_value().as_ref() != Some(value) { + prop.set_value(value_to_cst_input(value)); + } + } + None => { + object.append(name, value_to_cst_input(value)); + } + } + } +} + +fn value_to_cst_input(value: &Value) -> CstInputValue { + match value { + Value::Null => CstInputValue::Null, + Value::Bool(value) => CstInputValue::Bool(*value), + Value::Number(value) => CstInputValue::Number(value.to_string()), + Value::String(value) => CstInputValue::String(value.clone()), + Value::Array(values) => CstInputValue::Array( + values.iter().map(value_to_cst_input).collect(), + ), + Value::Object(values) => CstInputValue::Object( + values + .iter() + .map(|(key, value)| (key.clone(), value_to_cst_input(value))) + .collect(), + ), + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn parse_jsonc_accepts_comments() { + let parsed: Option = + parse_jsonc_opt(r#"{ "value": 1 } // comment"#).unwrap(); + + assert_eq!(parsed, Some(json!({ "value": 1 }))); + } + + #[test] + fn parse_jsonc_rejects_missing_commas() { + let result = parse_jsonc_opt::(r#"{ "a": 1 "b": 2 }"#); + + assert!(result.is_err()); + } + + #[test] + fn patch_jsonc_object_rejects_existing_non_object() { + let result = patch_jsonc_object(Some("[]"), &json!({ "a": 1 })); + + assert!(matches!(result, Err(JsonError::ExpectedObject))); + } + + #[test] + fn patch_jsonc_object_preserves_comments() { + let output = patch_jsonc_object( + Some( + r#"{ + // user's note + "provider": { + "old": true + }, + "keep": "value" +}"#, + ), + &json!({ + "provider": { + "old": false, + "new": true + }, + "keep": "value" + }), + ) + .unwrap(); + + assert!(output.contains("// user's note")); + assert!(output.contains(r#""old": false"#)); + assert!(output.contains(r#""new": true"#)); + assert!(output.contains(r#""keep": "value""#)); + } +} From 56aa46d0e0ce9a8ff668b894c2dc5726554246ff Mon Sep 17 00:00:00 2001 From: akarachen Date: Sun, 26 Apr 2026 14:23:02 +0800 Subject: [PATCH 18/62] feat(inference): wire opencode provider crud --- crates/api/src/bin/export-dto.rs | 13 +- crates/api/src/dto/inference.rs | 119 +++- crates/api/src/lib.rs | 4 + crates/api/src/routes/inference.rs | 98 ++- .../dto/AgentProviderCredentialDto.ts | 7 + .../dto/AgentProviderModelResponse.ts | 3 + .../generated/dto/AgentProviderResponse.ts | 16 + .../generated/dto/AgentProviderSourceDto.ts | 7 + .../dto/CreateAgentProviderRequest.ts | 3 + .../dto/InferenceProviderResponse.ts | 1 + .../dto/UpdateAgentProviderRequest.ts | 3 + crates/desktop/src/generated/dto/index.ts | 6 + crates/desktop/src/lib/api.ts | 31 + crates/desktop/src/lib/locales/en.ts | 23 + crates/desktop/src/lib/locales/zh-Hans.ts | 23 + crates/desktop/src/lib/locales/zh-Hant.ts | 23 + .../desktop/src/pages/inference-providers.tsx | 4 +- .../inference-providers/opencode-panel.tsx | 581 +++++++++++++++++- .../src/requests/inference-providers.ts | 94 +++ crates/desktop/src/requests/keys.ts | 2 + .../migrations/0003_add_masked_api_key.sql | 2 + crates/inference/src/agent.rs | 1 + crates/inference/src/model.rs | 3 + crates/inference/src/opencode/mod.rs | 31 + crates/inference/src/opencode/tests.rs | 21 + crates/inference/src/store.rs | 117 +++- 26 files changed, 1198 insertions(+), 38 deletions(-) create mode 100644 crates/desktop/src/generated/dto/AgentProviderCredentialDto.ts create mode 100644 crates/desktop/src/generated/dto/AgentProviderModelResponse.ts create mode 100644 crates/desktop/src/generated/dto/AgentProviderResponse.ts create mode 100644 crates/desktop/src/generated/dto/AgentProviderSourceDto.ts create mode 100644 crates/desktop/src/generated/dto/CreateAgentProviderRequest.ts create mode 100644 crates/desktop/src/generated/dto/UpdateAgentProviderRequest.ts create mode 100644 crates/inference/migrations/0003_add_masked_api_key.sql diff --git a/crates/api/src/bin/export-dto.rs b/crates/api/src/bin/export-dto.rs index 7b4490b5..33e50b7e 100644 --- a/crates/api/src/bin/export-dto.rs +++ b/crates/api/src/bin/export-dto.rs @@ -13,8 +13,11 @@ use aghub_api::dto::{ common::ConfigSource, credential::{CreateCredentialRequest, CredentialResponse}, inference::{ - CreateInferenceProviderRequest, InferenceProviderFormatDto, - InferenceProviderPasswordResponse, InferenceProviderResponse, + AgentProviderCredentialDto, AgentProviderModelResponse, + AgentProviderResponse, AgentProviderSourceDto, + CreateAgentProviderRequest, CreateInferenceProviderRequest, + InferenceProviderFormatDto, InferenceProviderPasswordResponse, + InferenceProviderResponse, UpdateAgentProviderRequest, UpdateInferenceProviderRequest, }, integrations::{ @@ -118,6 +121,12 @@ fn main() -> Result<(), Box> { export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; + export_type::(&cfg)?; + export_type::(&cfg)?; + export_type::(&cfg)?; + export_type::(&cfg)?; + export_type::(&cfg)?; + export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; diff --git a/crates/api/src/dto/inference.rs b/crates/api/src/dto/inference.rs index 4ae0e785..f6f6b248 100644 --- a/crates/api/src/dto/inference.rs +++ b/crates/api/src/dto/inference.rs @@ -1,6 +1,7 @@ use aghub_inference::{ - CreateInferenceProvider, InferenceProvider, InferenceProviderFormat, - UpdateInferenceProvider, + AgentProviderBinding, AgentProviderCredential, AgentProviderModel, + AgentProviderSource, CreateInferenceProvider, InferenceProvider, + InferenceProviderFormat, UpdateInferenceProvider, }; use serde::{Deserialize, Serialize}; use ts_rs::TS; @@ -93,6 +94,7 @@ pub struct InferenceProviderResponse { pub name: String, pub format: InferenceProviderFormatDto, pub api_base_url: String, + pub masked_api_key: String, pub models: Vec, } @@ -109,6 +111,7 @@ impl From<&InferenceProvider> for InferenceProviderResponse { name: provider.name.clone(), format: provider.format.into(), api_base_url: provider.api_base_url.clone(), + masked_api_key: provider.masked_api_key.clone(), models: provider.models.clone(), } } @@ -120,3 +123,115 @@ pub struct InferenceProviderPasswordResponse { pub name: String, pub api_key: String, } + +#[derive(Debug, Clone, Copy, Serialize, TS)] +#[ts(export)] +#[serde(rename_all = "snake_case")] +pub enum AgentProviderSourceDto { + ClosedSlot, + BuiltIn, + Custom, + StoredCredential, +} + +impl From for AgentProviderSourceDto { + fn from(value: AgentProviderSource) -> Self { + match value { + AgentProviderSource::ClosedSlot => Self::ClosedSlot, + AgentProviderSource::BuiltIn => Self::BuiltIn, + AgentProviderSource::Custom => Self::Custom, + AgentProviderSource::StoredCredential => Self::StoredCredential, + } + } +} + +#[derive(Debug, Clone, Serialize, TS)] +#[ts(export)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AgentProviderCredentialDto { + None, + EnvVar { name: String }, + AgentStore { id: Option }, + Inline, +} + +impl From<&AgentProviderCredential> for AgentProviderCredentialDto { + fn from(value: &AgentProviderCredential) -> Self { + match value { + AgentProviderCredential::None => Self::None, + AgentProviderCredential::EnvVar { name } => { + Self::EnvVar { name: name.clone() } + } + AgentProviderCredential::AgentStore { id } => { + Self::AgentStore { id: id.clone() } + } + AgentProviderCredential::Inline => Self::Inline, + } + } +} + +#[derive(Debug, Clone, Serialize, TS)] +#[ts(export)] +pub struct AgentProviderModelResponse { + pub id: String, + pub name: Option, +} + +impl From<&AgentProviderModel> for AgentProviderModelResponse { + fn from(value: &AgentProviderModel) -> Self { + Self { + id: value.id.clone(), + name: value.name.clone(), + } + } +} + +#[derive(Debug, Clone, Serialize, TS)] +#[ts(export)] +pub struct AgentProviderResponse { + pub id: String, + pub source_provider_id: Option, + pub name: String, + pub format: Option, + pub api_base_url: Option, + pub credential: AgentProviderCredentialDto, + pub models: Vec, + pub source: AgentProviderSourceDto, +} + +impl From for AgentProviderResponse { + fn from(provider: AgentProviderBinding) -> Self { + Self::from(&provider) + } +} + +impl From<&AgentProviderBinding> for AgentProviderResponse { + fn from(provider: &AgentProviderBinding) -> Self { + Self { + id: provider.id.clone(), + source_provider_id: provider.source_provider_id.clone(), + name: provider.name.clone(), + format: provider.format.map(Into::into), + api_base_url: provider.api_base_url.clone(), + credential: (&provider.credential).into(), + models: provider + .models + .iter() + .map(AgentProviderModelResponse::from) + .collect(), + source: provider.source.into(), + } + } +} + +#[derive(Debug, Deserialize, TS)] +#[ts(export)] +pub struct CreateAgentProviderRequest { + pub inference_provider_id: String, +} + +#[derive(Debug, Deserialize, TS)] +#[ts(export)] +pub struct UpdateAgentProviderRequest { + pub inference_provider_id: String, +} diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 2fe8ce49..383faa12 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -167,6 +167,10 @@ pub async fn start(options: ApiOptions) -> Result<(), rocket::Error> { routes::credentials::create_credential, routes::credentials::delete_credential, routes::inference::list_inference_providers, + routes::inference::list_opencode_providers, + routes::inference::create_opencode_provider, + routes::inference::update_opencode_provider, + routes::inference::delete_opencode_provider, routes::inference::get_inference_provider_password, routes::inference::create_inference_provider, routes::inference::update_inference_provider, diff --git a/crates/api/src/routes/inference.rs b/crates/api/src/routes/inference.rs index 2f68be1d..1b5ed910 100644 --- a/crates/api/src/routes/inference.rs +++ b/crates/api/src/routes/inference.rs @@ -1,5 +1,6 @@ use aghub_inference::{ - InferenceProvider, InferenceProviderRepository, InferenceProviderStore, + AgentProviderAdapter, InferenceProvider, InferenceProviderRepository, + InferenceProviderStore, OpenCodeProviderAdapter, }; use rocket::http::Status; use rocket::response::status::NoContent; @@ -7,8 +8,10 @@ use rocket::serde::json::Json; use rocket::State; use crate::dto::inference::{ + AgentProviderResponse, CreateAgentProviderRequest, CreateInferenceProviderRequest, InferenceProviderPasswordResponse, - InferenceProviderResponse, UpdateInferenceProviderRequest, + InferenceProviderResponse, UpdateAgentProviderRequest, + UpdateInferenceProviderRequest, }; use crate::error::{ApiCreated, ApiError, ApiNoContent, ApiResult}; use crate::state::InferenceProviderState; @@ -35,6 +38,31 @@ fn find_by_name( }) } +fn opencode_adapter() -> Result { + OpenCodeProviderAdapter::global().map_err(ApiError::from) +} + +fn get_inventory_provider( + store: &InferenceProviderStore, + id: &str, +) -> Result<(InferenceProvider, String), ApiError> { + let provider = store.get(id).map_err(ApiError::from)?; + let api_key = store + .get_api_key(&provider.id) + .map_err(ApiError::from)? + .ok_or_else(|| { + ApiError::new( + Status::UnprocessableEntity, + format!( + "inference provider '{}' has no stored API key", + provider.name + ), + "MISSING_CREDENTIAL", + ) + })?; + Ok((provider, api_key)) +} + #[get("/inference/providers")] pub fn list_inference_providers( state: &State, @@ -48,6 +76,72 @@ pub fn list_inference_providers( Ok(Json(providers)) } +#[get("/inference/agents/opencode/providers")] +pub fn list_opencode_providers() -> ApiResult> { + let providers = opencode_adapter()? + .load_providers() + .map_err(ApiError::from)? + .providers + .into_iter() + .map(AgentProviderResponse::from) + .collect(); + Ok(Json(providers)) +} + +#[post("/inference/agents/opencode/providers", data = "")] +pub fn create_opencode_provider( + state: &State, + body: Json, +) -> ApiCreated { + let store = store(state); + let (provider, api_key) = + get_inventory_provider(&store, &body.inference_provider_id)?; + let binding = opencode_adapter()? + .add_inventory_provider(&provider, &api_key) + .map_err(ApiError::from)?; + + Ok((Status::Created, Json(binding.into()))) +} + +#[put("/inference/agents/opencode/providers/", data = "")] +pub fn update_opencode_provider( + state: &State, + id: &str, + body: Json, +) -> ApiResult { + let store = store(state); + let (provider, api_key) = + get_inventory_provider(&store, &body.inference_provider_id)?; + let adapter = opencode_adapter()?; + let exists = adapter + .load_providers() + .map_err(ApiError::from)? + .providers + .iter() + .any(|provider| provider.id == id); + if !exists { + return Err(ApiError::new( + Status::NotFound, + format!("OpenCode provider '{id}' not found"), + "RESOURCE_NOT_FOUND", + )); + } + + let binding = adapter + .add_provider(id, &provider, &api_key) + .map_err(ApiError::from)?; + + Ok(Json(binding.into())) +} + +#[delete("/inference/agents/opencode/providers/")] +pub fn delete_opencode_provider(id: &str) -> ApiNoContent { + opencode_adapter()? + .remove_provider(id) + .map_err(ApiError::from)?; + Ok(NoContent) +} + #[get("/inference/providers//password")] pub fn get_inference_provider_password( state: &State, diff --git a/crates/desktop/src/generated/dto/AgentProviderCredentialDto.ts b/crates/desktop/src/generated/dto/AgentProviderCredentialDto.ts new file mode 100644 index 00000000..d15d2525 --- /dev/null +++ b/crates/desktop/src/generated/dto/AgentProviderCredentialDto.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AgentProviderCredentialDto = + | { type: "none" } + | { type: "env_var"; name: string } + | { type: "agent_store"; id: string | null } + | { type: "inline" }; diff --git a/crates/desktop/src/generated/dto/AgentProviderModelResponse.ts b/crates/desktop/src/generated/dto/AgentProviderModelResponse.ts new file mode 100644 index 00000000..68928650 --- /dev/null +++ b/crates/desktop/src/generated/dto/AgentProviderModelResponse.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AgentProviderModelResponse = { id: string; name: string | null }; diff --git a/crates/desktop/src/generated/dto/AgentProviderResponse.ts b/crates/desktop/src/generated/dto/AgentProviderResponse.ts new file mode 100644 index 00000000..183a221b --- /dev/null +++ b/crates/desktop/src/generated/dto/AgentProviderResponse.ts @@ -0,0 +1,16 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AgentProviderCredentialDto } from "./AgentProviderCredentialDto"; +import type { AgentProviderModelResponse } from "./AgentProviderModelResponse"; +import type { AgentProviderSourceDto } from "./AgentProviderSourceDto"; +import type { InferenceProviderFormatDto } from "./InferenceProviderFormatDto"; + +export type AgentProviderResponse = { + id: string; + source_provider_id: string | null; + name: string; + format: InferenceProviderFormatDto | null; + api_base_url: string | null; + credential: AgentProviderCredentialDto; + models: Array; + source: AgentProviderSourceDto; +}; diff --git a/crates/desktop/src/generated/dto/AgentProviderSourceDto.ts b/crates/desktop/src/generated/dto/AgentProviderSourceDto.ts new file mode 100644 index 00000000..992e3a0d --- /dev/null +++ b/crates/desktop/src/generated/dto/AgentProviderSourceDto.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AgentProviderSourceDto = + | "closed_slot" + | "built_in" + | "custom" + | "stored_credential"; diff --git a/crates/desktop/src/generated/dto/CreateAgentProviderRequest.ts b/crates/desktop/src/generated/dto/CreateAgentProviderRequest.ts new file mode 100644 index 00000000..f6598bb2 --- /dev/null +++ b/crates/desktop/src/generated/dto/CreateAgentProviderRequest.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CreateAgentProviderRequest = { inference_provider_id: string }; diff --git a/crates/desktop/src/generated/dto/InferenceProviderResponse.ts b/crates/desktop/src/generated/dto/InferenceProviderResponse.ts index 73e94feb..9dbb0365 100644 --- a/crates/desktop/src/generated/dto/InferenceProviderResponse.ts +++ b/crates/desktop/src/generated/dto/InferenceProviderResponse.ts @@ -6,5 +6,6 @@ export type InferenceProviderResponse = { name: string; format: InferenceProviderFormatDto; api_base_url: string; + masked_api_key: string; models: Array; }; diff --git a/crates/desktop/src/generated/dto/UpdateAgentProviderRequest.ts b/crates/desktop/src/generated/dto/UpdateAgentProviderRequest.ts new file mode 100644 index 00000000..7c6a3a76 --- /dev/null +++ b/crates/desktop/src/generated/dto/UpdateAgentProviderRequest.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type UpdateAgentProviderRequest = { inference_provider_id: string }; diff --git a/crates/desktop/src/generated/dto/index.ts b/crates/desktop/src/generated/dto/index.ts index cac5c313..dda3b552 100644 --- a/crates/desktop/src/generated/dto/index.ts +++ b/crates/desktop/src/generated/dto/index.ts @@ -1,8 +1,13 @@ export type { AgentAvailabilityDto } from "./AgentAvailabilityDto"; export type { AgentInfo } from "./AgentInfo"; +export type { AgentProviderCredentialDto } from "./AgentProviderCredentialDto"; +export type { AgentProviderModelResponse } from "./AgentProviderModelResponse"; +export type { AgentProviderResponse } from "./AgentProviderResponse"; +export type { AgentProviderSourceDto } from "./AgentProviderSourceDto"; export type { CapabilitiesDto } from "./CapabilitiesDto"; export type { CodeEditorType } from "./CodeEditorType"; export type { ConfigSource } from "./ConfigSource"; +export type { CreateAgentProviderRequest } from "./CreateAgentProviderRequest"; export type { CreateCredentialRequest } from "./CreateCredentialRequest"; export type { CreateInferenceProviderRequest } from "./CreateInferenceProviderRequest"; export type { CreateMcpRequest } from "./CreateMcpRequest"; @@ -57,6 +62,7 @@ export type { ToolInfoDto } from "./ToolInfoDto"; export type { ToolPreferencesDto } from "./ToolPreferencesDto"; export type { TransferRequest } from "./TransferRequest"; export type { TransportDto } from "./TransportDto"; +export type { UpdateAgentProviderRequest } from "./UpdateAgentProviderRequest"; export type { UpdateInferenceProviderRequest } from "./UpdateInferenceProviderRequest"; export type { UpdateMcpRequest } from "./UpdateMcpRequest"; export type { UpdateSkillRequest } from "./UpdateSkillRequest"; diff --git a/crates/desktop/src/lib/api.ts b/crates/desktop/src/lib/api.ts index 074dd73c..cbc88bbb 100644 --- a/crates/desktop/src/lib/api.ts +++ b/crates/desktop/src/lib/api.ts @@ -2,7 +2,9 @@ import ky, { isHTTPError } from "ky"; import type { AgentAvailabilityDto, AgentInfo, + AgentProviderResponse, CodeEditorType, + CreateAgentProviderRequest, CreateCredentialRequest, CreateInferenceProviderRequest, CreateMcpRequest, @@ -33,6 +35,7 @@ import type { SubAgentResponse, ToolInfoDto, TransferRequest, + UpdateAgentProviderRequest, UpdateInferenceProviderRequest, UpdateMcpRequest, UpdateSubAgentRequest, @@ -482,6 +485,9 @@ export function createApi(baseUrl: string) { list(): Promise { return client.get("inference/providers").json(); }, + listOpenCode(): Promise { + return client.get("inference/agents/opencode/providers").json(); + }, getPassword( name: string, ): Promise { @@ -498,6 +504,13 @@ export function createApi(baseUrl: string) { .post("inference/providers", { json: body }) .json(); }, + createOpenCode( + body: CreateAgentProviderRequest, + ): Promise { + return client + .post("inference/agents/opencode/providers", { json: body }) + .json(); + }, update( name: string, body: UpdateInferenceProviderRequest, @@ -508,6 +521,17 @@ export function createApi(baseUrl: string) { }) .json(); }, + updateOpenCode( + id: string, + body: UpdateAgentProviderRequest, + ): Promise { + return client + .put( + `inference/agents/opencode/providers/${encodeURIComponent(id)}`, + { json: body }, + ) + .json(); + }, async createModel( providerName: string, modelName: string, @@ -552,6 +576,13 @@ export function createApi(baseUrl: string) { .delete(`inference/providers/${encodeURIComponent(name)}`) .then(() => undefined); }, + deleteOpenCode(id: string): Promise { + return client + .delete( + `inference/agents/opencode/providers/${encodeURIComponent(id)}`, + ) + .then(() => undefined); + }, }, }; } diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index 7e0c1348..8c15f7ac 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -93,6 +93,29 @@ export default { "Choose your preferred code editor for opening files", inferenceProviders: "Inference Providers", codingAgents: "Coding Agents", + openCodeProvidersDescription: + "Providers configured in opencode.json and auth.json.", + createOpenCodeProvider: "Add OpenCode Provider", + editOpenCodeProvider: "Edit OpenCode Provider", + deleteOpenCodeProvider: "Delete OpenCode Provider", + deleteOpenCodeProviderConfirm: + 'Delete "{{name}}" from OpenCode? Its OpenCode auth entry will also be removed.', + refreshOpenCodeProviders: "Refresh OpenCode providers", + noOpenCodeProviders: "No OpenCode providers configured.", + selectInferenceProvider: "Inference provider", + noInferenceProvidersForOpenCode: + "Create an inference provider first, then select it here.", + openCodeProviderCreated: "OpenCode provider created", + openCodeProviderUpdated: "OpenCode provider updated", + openCodeProviderDeleted: "OpenCode provider deleted", + openCodeProviderDeleteError: "Failed to delete OpenCode provider", + agentProviderSourceCustom: "Custom", + agentProviderSourceBuiltIn: "Built-in", + agentProviderSourceClosedSlot: "Closed slot", + agentProviderSourceStoredCredential: "Credential only", + agentProviderCredentialAgentStore: "Agent store", + agentProviderCredentialInline: "Inline key", + agentProviderCredentialNone: "No credential", searchInferenceProviderResources: "Search agents or providers...", searchInferenceProviders: "Search providers...", refreshInferenceProviders: "Refresh providers", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index 9965e748..441a544d 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -89,6 +89,29 @@ export default { codeEditorsDescription: "选择用于打开文件的首选代码编辑器", inferenceProviders: "推理 Provider", codingAgents: "Coding Agents", + openCodeProvidersDescription: + "配置在 opencode.json 和 auth.json 里的 Provider。", + createOpenCodeProvider: "添加 OpenCode Provider", + editOpenCodeProvider: "编辑 OpenCode Provider", + deleteOpenCodeProvider: "删除 OpenCode Provider", + deleteOpenCodeProviderConfirm: + '确定从 OpenCode 删除"{{name}}"吗?对应的 OpenCode auth 也会被移除。', + refreshOpenCodeProviders: "刷新 OpenCode Provider", + noOpenCodeProviders: "暂无 OpenCode Provider。", + selectInferenceProvider: "推理 Provider", + noInferenceProvidersForOpenCode: + "请先创建一个推理 Provider,然后在这里选择。", + openCodeProviderCreated: "OpenCode Provider 已创建", + openCodeProviderUpdated: "OpenCode Provider 已更新", + openCodeProviderDeleted: "OpenCode Provider 已删除", + openCodeProviderDeleteError: "删除 OpenCode Provider 失败", + agentProviderSourceCustom: "自定义", + agentProviderSourceBuiltIn: "内置", + agentProviderSourceClosedSlot: "封闭槽位", + agentProviderSourceStoredCredential: "仅凭据", + agentProviderCredentialAgentStore: "Agent 存储", + agentProviderCredentialInline: "内联 key", + agentProviderCredentialNone: "无凭据", searchInferenceProviderResources: "搜索 Agent 或 Provider...", searchInferenceProviders: "搜索 Provider...", refreshInferenceProviders: "刷新 Provider", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index 53b97797..d921d16b 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -89,6 +89,29 @@ export default { codeEditorsDescription: "選擇用於開啟檔案的偏好程式碼編輯器", inferenceProviders: "推理 Provider", codingAgents: "Coding Agents", + openCodeProvidersDescription: + "配置在 opencode.json 和 auth.json 裡的 Provider。", + createOpenCodeProvider: "新增 OpenCode Provider", + editOpenCodeProvider: "編輯 OpenCode Provider", + deleteOpenCodeProvider: "刪除 OpenCode Provider", + deleteOpenCodeProviderConfirm: + "確定從 OpenCode 刪除「{{name}}」嗎?對應的 OpenCode auth 也會被移除。", + refreshOpenCodeProviders: "重新整理 OpenCode Provider", + noOpenCodeProviders: "尚無 OpenCode Provider。", + selectInferenceProvider: "推理 Provider", + noInferenceProvidersForOpenCode: + "請先建立一個推理 Provider,然後在這裡選擇。", + openCodeProviderCreated: "OpenCode Provider 已建立", + openCodeProviderUpdated: "OpenCode Provider 已更新", + openCodeProviderDeleted: "OpenCode Provider 已刪除", + openCodeProviderDeleteError: "刪除 OpenCode Provider 失敗", + agentProviderSourceCustom: "自訂", + agentProviderSourceBuiltIn: "內建", + agentProviderSourceClosedSlot: "封閉槽位", + agentProviderSourceStoredCredential: "僅憑證", + agentProviderCredentialAgentStore: "Agent 儲存", + agentProviderCredentialInline: "內嵌 key", + agentProviderCredentialNone: "無憑證", searchInferenceProviderResources: "搜尋 Agent 或 Provider...", searchInferenceProviders: "搜尋 Provider...", refreshInferenceProviders: "重新整理 Provider", diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index 48b2e68a..71fbe04b 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -642,6 +642,7 @@ function ProviderDetail({ const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [revealedKey, setRevealedKey] = useState(null); const [isCopying, setIsCopying] = useState(false); + const previewKey = provider.masked_api_key || "••••••••••••••••••••••••"; const passwordMutation = useMutation({ mutationFn: (name: string) => api.inferenceProviders.getPassword(name), @@ -792,8 +793,7 @@ function ProviderDetail({
- {revealedKey ?? - "••••••••••••••••••••••••"} + {revealedKey ?? previewKey} + +
+ + ); +} + +function ProviderRow({ + provider, + onEdit, + onDelete, +}: { + provider: AgentProviderResponse; + onEdit: () => void; + onDelete: () => void; +}) { + const { t } = useTranslation(); + const credential = credentialLabel(provider); + + return ( +
+
+
+ + + {provider.id !== provider.name && ( + + {provider.id} + + )} +
+
+ {t(formatLabelKey(provider.format))} + {provider.api_base_url && ( + + {provider.api_base_url} + + )} + + {t("providerModels")}: {provider.models.length} + + {t(sourceLabelKey(provider.source))} + + {credential.startsWith("agentProvider") + ? t(credential) + : credential} + +
+
+ +
+ + + + + {t("edit")} + + + + + + {t("delete")} + +
+
+ ); +} export function OpenCodeInferenceProviderPanel() { + const { t } = useTranslation(); + const api = useApi(); + const queryClient = useQueryClient(); + const [mode, setMode] = useState({ type: "list" }); + const [deleteTarget, setDeleteTarget] = + useState(null); + + const { + data: providers = [], + isLoading, + isFetching, + refetch, + } = useQuery({ + ...openCodeProviderListQueryOptions({ api }), + }); + const { data: inventoryProviders = [], isLoading: isInventoryLoading } = + useQuery({ + ...inferenceProviderListQueryOptions({ api }), + }); + + const activeProvider = useMemo(() => { + if (mode.type !== "edit") return undefined; + return mode.provider; + }, [mode]); + + const deleteMutation = useMutation({ + ...deleteOpenCodeProviderMutationOptions({ + api, + queryClient, + onSuccess: async () => { + setDeleteTarget(null); + toast.success(t("openCodeProviderDeleted")); + }, + }), + onError: (error) => { + console.error("Failed to delete OpenCode provider:", error); + toast.danger( + error instanceof Error + ? error.message + : t("openCodeProviderDeleteError"), + ); + }, + }); + + const showForm = mode.type === "create" || mode.type === "edit"; + return ( -
-
- - -
- -
-

- OpenCode -

+ <> +
+
+ + +
+ +
+

+ OpenCode +

+
-
- - - +
+ + + + + + {t("refresh")} + + + +
+ + + + {isLoading || isInventoryLoading ? ( +
+ +
+ ) : ( + <> + {showForm && ( +
+
+

+ {mode.type === "create" + ? t( + "createOpenCodeProvider", + ) + : t( + "editOpenCodeProvider", + )} +

+
+ + setMode({ type: "list" }) + } + onSaved={() => + setMode({ type: "list" }) + } + /> +
+ )} + + {providers.length === 0 ? ( +
+

+ {t("noOpenCodeProviders")} +

+ +
+ ) : ( +
+ {providers.map((provider) => ( + + setMode({ + type: "edit", + provider, + }) + } + onDelete={() => + setDeleteTarget( + provider, + ) + } + /> + ))} +
+ )} + + )} +
+ +
-
+ + { + if (!open) setDeleteTarget(null); + }} + > + + + + + + + {t("deleteOpenCodeProvider")} + + + + {t("deleteOpenCodeProviderConfirm", { + name: deleteTarget?.name, + })} + + + + + + + + + ); } diff --git a/crates/desktop/src/requests/inference-providers.ts b/crates/desktop/src/requests/inference-providers.ts index 7bf56077..0df18081 100644 --- a/crates/desktop/src/requests/inference-providers.ts +++ b/crates/desktop/src/requests/inference-providers.ts @@ -4,8 +4,11 @@ import { queryOptions, } from "@tanstack/react-query"; import type { + AgentProviderResponse, + CreateAgentProviderRequest, CreateInferenceProviderRequest, InferenceProviderResponse, + UpdateAgentProviderRequest, UpdateInferenceProviderRequest, } from "../generated/dto"; import type { ApiClient } from "./client"; @@ -30,6 +33,19 @@ export function inferenceProviderListQueryOptions({ }); } +export function openCodeProviderListQueryOptions({ + api, + enabled = true, + staleTime = 30_000, +}: InferenceProviderListQueryParams) { + return queryOptions({ + queryKey: queryKeys.inferenceProviders.agent("opencode"), + queryFn: () => api.inferenceProviders.listOpenCode(), + enabled, + staleTime, + }); +} + export async function invalidateInferenceProviderQueries( queryClient: QueryClient, ) { @@ -38,6 +54,14 @@ export async function invalidateInferenceProviderQueries( }); } +export async function invalidateOpenCodeProviderQueries( + queryClient: QueryClient, +) { + await queryClient.invalidateQueries({ + queryKey: queryKeys.inferenceProviders.agent("opencode"), + }); +} + interface CreateInferenceProviderMutationParams { api: ApiClient; queryClient: QueryClient; @@ -59,6 +83,27 @@ export function createInferenceProviderMutationOptions({ }); } +interface CreateAgentProviderMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: (data: AgentProviderResponse) => void | Promise; +} + +export function createOpenCodeProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: CreateAgentProviderMutationParams) { + return mutationOptions({ + mutationFn: (body: CreateAgentProviderRequest) => + api.inferenceProviders.createOpenCode(body), + onSuccess: async (data) => { + await invalidateOpenCodeProviderQueries(queryClient); + await onSuccess?.(data); + }, + }); +} + interface CreateInferenceModelVariables { providerName: string; modelName: string; @@ -120,6 +165,35 @@ export function updateInferenceProviderMutationOptions({ }); } +interface UpdateAgentProviderVariables { + id: string; + body: UpdateAgentProviderRequest; +} + +interface UpdateAgentProviderMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: ( + data: AgentProviderResponse, + variables: UpdateAgentProviderVariables, + ) => void | Promise; +} + +export function updateOpenCodeProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: UpdateAgentProviderMutationParams) { + return mutationOptions({ + mutationFn: ({ id, body }: UpdateAgentProviderVariables) => + api.inferenceProviders.updateOpenCode(id, body), + onSuccess: async (data, variables) => { + await invalidateOpenCodeProviderQueries(queryClient); + await onSuccess?.(data, variables); + }, + }); +} + interface UpdateInferenceModelVariables { providerName: string; modelName: string; @@ -178,6 +252,26 @@ export function deleteInferenceProviderMutationOptions({ }); } +interface DeleteAgentProviderMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: () => void | Promise; +} + +export function deleteOpenCodeProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: DeleteAgentProviderMutationParams) { + return mutationOptions({ + mutationFn: (id: string) => api.inferenceProviders.deleteOpenCode(id), + onSuccess: async () => { + await invalidateOpenCodeProviderQueries(queryClient); + await onSuccess?.(); + }, + }); +} + interface DeleteInferenceModelVariables { providerName: string; modelName: string; diff --git a/crates/desktop/src/requests/keys.ts b/crates/desktop/src/requests/keys.ts index dc1a4bb9..708eada3 100644 --- a/crates/desktop/src/requests/keys.ts +++ b/crates/desktop/src/requests/keys.ts @@ -52,6 +52,8 @@ export const queryKeys = { inferenceProviders: { all: () => ["inference-providers"] as const, list: () => ["inference-providers", "list"] as const, + agent: (agentId: string) => + ["inference-providers", "agent", agentId] as const, password: (name: string) => ["inference-providers", "password", name] as const, }, diff --git a/crates/inference/migrations/0003_add_masked_api_key.sql b/crates/inference/migrations/0003_add_masked_api_key.sql new file mode 100644 index 00000000..f89915ae --- /dev/null +++ b/crates/inference/migrations/0003_add_masked_api_key.sql @@ -0,0 +1,2 @@ +ALTER TABLE inference_providers +ADD COLUMN masked_api_key TEXT NOT NULL DEFAULT ''; diff --git a/crates/inference/src/agent.rs b/crates/inference/src/agent.rs index 4a2dfda1..7156ab4f 100644 --- a/crates/inference/src/agent.rs +++ b/crates/inference/src/agent.rs @@ -629,6 +629,7 @@ mod tests { name: "OpenRouter".to_string(), format: InferenceProviderFormat::OpenAiResponses, api_base_url: "https://openrouter.ai/api/v1".to_string(), + masked_api_key: "sk****st".to_string(), models: vec![ "openai/gpt-5.4".to_string(), "anthropic/claude-sonnet-4-5".to_string(), diff --git a/crates/inference/src/model.rs b/crates/inference/src/model.rs index cff5bd87..a022857d 100644 --- a/crates/inference/src/model.rs +++ b/crates/inference/src/model.rs @@ -77,6 +77,9 @@ pub struct InferenceProvider { /// Base URL for provider API requests. pub api_base_url: String, + /// Masked API key stored for preview only. + pub masked_api_key: String, + /// Model names supported by this provider. pub models: Vec, } diff --git a/crates/inference/src/opencode/mod.rs b/crates/inference/src/opencode/mod.rs index f5f0eb0f..b2f1c8b8 100644 --- a/crates/inference/src/opencode/mod.rs +++ b/crates/inference/src/opencode/mod.rs @@ -113,6 +113,37 @@ impl OpenCodeProviderAdapter { self.add_provider(&provider_id, provider, api_key) } + /// Remove a provider definition and matching OpenCode auth entry. + pub fn remove_provider( + &self, + provider_id: &str, + ) -> Result { + let provider_id = mapping::clean_provider_id(provider_id)?; + let mut state = self.load_providers()?; + let removed = state + .providers + .iter() + .find(|provider| provider.id == provider_id) + .cloned() + .ok_or_else(|| { + crate::error::InferenceProviderError::NotFound( + provider_id.clone(), + ) + })?; + + state + .providers + .retain(|provider| provider.id != provider_id); + self.save_providers(&state)?; + + let mut auth = files::read_auth_values(&self.auth_path)?; + if auth.remove(&provider_id).is_some() { + files::write_auth_values(&self.auth_path, &auth)?; + } + + Ok(removed) + } + fn set_api_auth(&self, provider_id: &str, api_key: &str) -> Result<()> { let mut auth = files::read_auth_values(&self.auth_path)?; auth.insert( diff --git a/crates/inference/src/opencode/tests.rs b/crates/inference/src/opencode/tests.rs index 88b87682..61dec77c 100644 --- a/crates/inference/src/opencode/tests.rs +++ b/crates/inference/src/opencode/tests.rs @@ -19,6 +19,7 @@ fn provider() -> InferenceProvider { name: "OpenRouter".to_string(), format: InferenceProviderFormat::OpenAiCompletions, api_base_url: "https://openrouter.ai/api/v1".to_string(), + masked_api_key: "sk****st".to_string(), models: vec!["anthropic/claude-sonnet-4-5".to_string()], } } @@ -221,6 +222,26 @@ fn save_preserves_auth_only_entries_without_configuring_them() { assert!(config.get("provider").is_none()); } +#[test] +fn remove_provider_deletes_config_and_auth_entry() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + adapter + .add_provider("openrouter", &provider(), "sk-test") + .unwrap(); + + let removed = adapter.remove_provider("openrouter").unwrap(); + + assert_eq!(removed.id, "openrouter"); + let state = adapter.load_providers().unwrap(); + assert!(state.providers.is_empty()); + let auth: Option = aghub_json::parse_jsonc_opt( + &fs::read_to_string(adapter.auth_path()).unwrap(), + ) + .unwrap(); + assert!(auth.unwrap().get("openrouter").is_none()); +} + #[test] fn jsonc_comments_are_accepted() { let temp = tempfile::tempdir().unwrap(); diff --git a/crates/inference/src/store.rs b/crates/inference/src/store.rs index 21ff679c..0d511c8c 100644 --- a/crates/inference/src/store.rs +++ b/crates/inference/src/store.rs @@ -143,7 +143,7 @@ impl InferenceProviderStore { id: &str, ) -> Result { let row = sqlx::query( - "SELECT id, name, format, api_base_url \ + "SELECT id, name, format, api_base_url, masked_api_key \ FROM inference_providers WHERE id = ?", ) .bind(id) @@ -239,6 +239,7 @@ fn map_row(row: sqlx::sqlite::SqliteRow) -> Result { name: row.try_get("name")?, format, api_base_url: row.try_get("api_base_url")?, + masked_api_key: row.try_get("masked_api_key")?, models: Vec::new(), }) } @@ -250,7 +251,7 @@ impl InferenceProviderRepository self.block_on(async { let mut conn = self.open_db().await?; let rows = sqlx::query( - "SELECT id, name, format, api_base_url \ + "SELECT id, name, format, api_base_url, masked_api_key \ FROM inference_providers ORDER BY rowid", ) .fetch_all(&mut conn) @@ -291,6 +292,7 @@ impl InferenceProviderRepository name, format: input.format, api_base_url, + masked_api_key: mask_api_key(&input.api_key), models, }; @@ -299,13 +301,14 @@ impl InferenceProviderRepository let result: Result<()> = async { sqlx::query( "INSERT INTO inference_providers \ - (id, name, format, api_base_url) \ - VALUES (?, ?, ?, ?)", + (id, name, format, api_base_url, masked_api_key) \ + VALUES (?, ?, ?, ?, ?)", ) .bind(&provider.id) .bind(&provider.name) .bind(provider.format.to_string()) .bind(&provider.api_base_url) + .bind(&provider.masked_api_key) .execute(&mut conn) .await?; Self::replace_models(&mut conn, &provider.id, &provider.models) @@ -366,6 +369,7 @@ impl InferenceProviderRepository ensure_api_key(api_key)?; let previous = self.credentials.get_api_key(id)?; self.credentials.set_api_key(id, api_key)?; + provider.masked_api_key = mask_api_key(api_key); Some(previous) } None => None, @@ -374,12 +378,14 @@ impl InferenceProviderRepository let result: Result<()> = async { sqlx::query( "UPDATE inference_providers \ - SET name = ?, format = ?, api_base_url = ? \ + SET name = ?, format = ?, api_base_url = ?, \ + masked_api_key = ? \ WHERE id = ?", ) .bind(&provider.name) .bind(provider.format.to_string()) .bind(&provider.api_base_url) + .bind(&provider.masked_api_key) .bind(id) .execute(&mut conn) .await?; @@ -440,14 +446,62 @@ impl InferenceProviderRepository } fn set_api_key(&self, id: &str, api_key: &str) -> Result<()> { - self.get(id)?; ensure_api_key(api_key)?; - self.credentials.set_api_key(id, api_key) + self.block_on(async { + let mut conn = self.open_db().await?; + Self::fetch_by_id(&mut conn, id).await?; + let previous = self.credentials.get_api_key(id)?; + self.credentials.set_api_key(id, api_key)?; + + let result = sqlx::query( + "UPDATE inference_providers SET masked_api_key = ? \ + WHERE id = ?", + ) + .bind(mask_api_key(api_key)) + .bind(id) + .execute(&mut conn) + .await; + + if let Err(error) = result { + match previous { + Some(key) => { + let _ = self.credentials.set_api_key(id, &key); + } + None => { + let _ = self.credentials.delete_api_key(id); + } + } + return Err(error.into()); + } + + Ok(()) + }) } fn delete_api_key(&self, id: &str) -> Result<()> { - self.get(id)?; - self.credentials.delete_api_key(id) + self.block_on(async { + let mut conn = self.open_db().await?; + Self::fetch_by_id(&mut conn, id).await?; + let previous = self.credentials.get_api_key(id)?; + self.credentials.delete_api_key(id)?; + + let result = sqlx::query( + "UPDATE inference_providers SET masked_api_key = '' \ + WHERE id = ?", + ) + .bind(id) + .execute(&mut conn) + .await; + + if let Err(error) = result { + if let Some(key) = previous { + let _ = self.credentials.set_api_key(id, &key); + } + return Err(error.into()); + } + + Ok(()) + }) } } @@ -492,6 +546,23 @@ fn ensure_api_key(api_key: &str) -> Result<()> { } } +fn mask_api_key(api_key: &str) -> String { + let value = api_key.trim(); + let chars = value.chars().collect::>(); + let len = chars.len(); + + if len <= 4 { + return "*".repeat(len); + } + + let visible_each = (len / 6).clamp(1, 6); + let mask_len = len.saturating_sub(visible_each * 2); + let prefix = chars.iter().take(visible_each).collect::(); + let suffix = chars.iter().skip(len - visible_each).collect::(); + + format!("{prefix}{}{suffix}", "*".repeat(mask_len)) +} + fn clean_api_base_url(api_base_url: &str) -> Result { let api_base_url = api_base_url.trim(); if api_base_url.is_empty() { @@ -586,6 +657,7 @@ mod tests { assert_eq!(fetched.name, "OpenAI"); assert_eq!(fetched.format, InferenceProviderFormat::OpenAiResponses); assert_eq!(fetched.api_base_url, "https://api.openai.com/v1"); + assert_eq!(fetched.masked_api_key, "s*****t"); assert!(fetched.models.is_empty()); // API key is kept in the credential store, not in provider metadata @@ -600,6 +672,17 @@ mod tests { ); } + #[test] + fn test_mask_api_key_uses_length_ratio() { + assert_eq!(mask_api_key("abc"), "***"); + assert_eq!(mask_api_key("abcde"), "a***e"); + assert_eq!(mask_api_key("sk-test"), "s*****t"); + assert_eq!( + mask_api_key("sk-v1-abcdefghijklmnopqrstuvwxyz"), + "sk-v1**********************vwxyz" + ); + } + #[test] fn test_update_provider_metadata_and_api_key() { let (_temp, store) = store(); @@ -631,6 +714,7 @@ mod tests { assert_eq!(updated.name, "Claude"); assert_eq!(updated.format, InferenceProviderFormat::OpenAiCompletions); assert_eq!(updated.api_base_url, "https://gateway.example.com/v1"); + assert_eq!(updated.masked_api_key, "s********y"); assert_eq!( store.get_api_key(&provider.id).unwrap(), Some("second-key".to_string()) @@ -657,6 +741,21 @@ mod tests { assert_eq!(store.credentials.get_api_key(&provider.id).unwrap(), None); } + #[test] + fn test_set_and_delete_api_key_updates_masked_preview() { + let (_temp, store) = store(); + let provider = create_provider(&store, "OpenAI"); + + store.set_api_key(&provider.id, "replacement-key").unwrap(); + assert_eq!( + store.get(&provider.id).unwrap().masked_api_key, + "re***********ey" + ); + + store.delete_api_key(&provider.id).unwrap(); + assert_eq!(store.get(&provider.id).unwrap().masked_api_key, ""); + } + #[test] fn test_duplicate_name_is_rejected() { let (_temp, store) = store(); From c33f68ca7f43ff5058d399b023869e4136deebd4 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sun, 26 Apr 2026 15:00:13 +0800 Subject: [PATCH 19/62] feat(inference): manage opencode providers Add OpenCode provider CRUD backed by auth.json and opencode.json, plus matching/sync against stored inference providers. Split inference provider stable names from display names for UI editing. --- crates/api/src/bin/export-dto.rs | 14 +- crates/api/src/dto/inference.rs | 45 +- crates/api/src/lib.rs | 1 + crates/api/src/routes/inference.rs | 137 +++- ...roviderMatchedInferenceProviderResponse.ts | 8 + .../generated/dto/AgentProviderResponse.ts | 2 + .../dto/CreateInferenceProviderRequest.ts | 1 + .../dto/InferenceProviderResponse.ts | 1 + .../dto/UpdateAgentProviderRequest.ts | 5 +- .../dto/UpdateInferenceProviderRequest.ts | 1 + crates/desktop/src/generated/dto/index.ts | 1 + crates/desktop/src/lib/api.ts | 8 + crates/desktop/src/lib/locales/en.ts | 8 + crates/desktop/src/lib/locales/zh-Hans.ts | 7 + crates/desktop/src/lib/locales/zh-Hant.ts | 7 + .../desktop/src/pages/inference-providers.tsx | 25 +- .../inference-providers/opencode-panel.tsx | 600 +++++++++++------- .../src/requests/inference-providers.ts | 23 + .../migrations/0004_add_display_name.sql | 6 + crates/inference/src/agent.rs | 6 +- crates/inference/src/model.rs | 17 +- crates/inference/src/opencode/mapping.rs | 9 + crates/inference/src/opencode/mod.rs | 91 ++- crates/inference/src/opencode/tests.rs | 102 +++ crates/inference/src/store.rs | 49 +- 25 files changed, 891 insertions(+), 283 deletions(-) create mode 100644 crates/desktop/src/generated/dto/AgentProviderMatchedInferenceProviderResponse.ts create mode 100644 crates/inference/migrations/0004_add_display_name.sql diff --git a/crates/api/src/bin/export-dto.rs b/crates/api/src/bin/export-dto.rs index 33e50b7e..a477e589 100644 --- a/crates/api/src/bin/export-dto.rs +++ b/crates/api/src/bin/export-dto.rs @@ -13,12 +13,13 @@ use aghub_api::dto::{ common::ConfigSource, credential::{CreateCredentialRequest, CredentialResponse}, inference::{ - AgentProviderCredentialDto, AgentProviderModelResponse, - AgentProviderResponse, AgentProviderSourceDto, - CreateAgentProviderRequest, CreateInferenceProviderRequest, - InferenceProviderFormatDto, InferenceProviderPasswordResponse, - InferenceProviderResponse, UpdateAgentProviderRequest, - UpdateInferenceProviderRequest, + AgentProviderCredentialDto, + AgentProviderMatchedInferenceProviderResponse, + AgentProviderModelResponse, AgentProviderResponse, + AgentProviderSourceDto, CreateAgentProviderRequest, + CreateInferenceProviderRequest, InferenceProviderFormatDto, + InferenceProviderPasswordResponse, InferenceProviderResponse, + UpdateAgentProviderRequest, UpdateInferenceProviderRequest, }, integrations::{ CodeEditorType, EditSkillFolderRequest, OpenSkillFolderRequest, @@ -124,6 +125,7 @@ fn main() -> Result<(), Box> { export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; + export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; diff --git a/crates/api/src/dto/inference.rs b/crates/api/src/dto/inference.rs index f6f6b248..a37f144a 100644 --- a/crates/api/src/dto/inference.rs +++ b/crates/api/src/dto/inference.rs @@ -47,6 +47,7 @@ impl From for InferenceProviderFormat { #[ts(export)] pub struct CreateInferenceProviderRequest { pub name: String, + pub display_name: String, pub format: InferenceProviderFormatDto, pub api_base_url: String, pub api_key: String, @@ -57,6 +58,7 @@ impl From for CreateInferenceProvider { fn from(req: CreateInferenceProviderRequest) -> Self { CreateInferenceProvider { name: req.name, + display_name: req.display_name, format: req.format.into(), api_base_url: req.api_base_url, api_key: req.api_key, @@ -69,6 +71,7 @@ impl From for CreateInferenceProvider { #[ts(export)] pub struct UpdateInferenceProviderRequest { pub name: Option, + pub display_name: Option, pub format: Option, pub api_base_url: Option, pub api_key: Option, @@ -79,6 +82,7 @@ impl From for UpdateInferenceProvider { fn from(req: UpdateInferenceProviderRequest) -> Self { UpdateInferenceProvider { name: req.name, + display_name: req.display_name, format: req.format.map(Into::into), api_base_url: req.api_base_url, api_key: req.api_key, @@ -92,6 +96,7 @@ impl From for UpdateInferenceProvider { pub struct InferenceProviderResponse { pub id: String, pub name: String, + pub display_name: String, pub format: InferenceProviderFormatDto, pub api_base_url: String, pub masked_api_key: String, @@ -109,6 +114,7 @@ impl From<&InferenceProvider> for InferenceProviderResponse { InferenceProviderResponse { id: provider.id.clone(), name: provider.name.clone(), + display_name: provider.display_name.clone(), format: provider.format.into(), api_base_url: provider.api_base_url.clone(), masked_api_key: provider.masked_api_key.clone(), @@ -186,6 +192,28 @@ impl From<&AgentProviderModel> for AgentProviderModelResponse { } } +#[derive(Debug, Clone, Serialize, TS)] +#[ts(export)] +pub struct AgentProviderMatchedInferenceProviderResponse { + pub id: String, + pub name: String, + pub display_name: String, + pub model_count: usize, +} + +impl From<&InferenceProvider> + for AgentProviderMatchedInferenceProviderResponse +{ + fn from(provider: &InferenceProvider) -> Self { + Self { + id: provider.id.clone(), + name: provider.name.clone(), + display_name: provider.display_name.clone(), + model_count: provider.models.len(), + } + } +} + #[derive(Debug, Clone, Serialize, TS)] #[ts(export)] pub struct AgentProviderResponse { @@ -197,6 +225,8 @@ pub struct AgentProviderResponse { pub credential: AgentProviderCredentialDto, pub models: Vec, pub source: AgentProviderSourceDto, + pub matched_inference_provider: + Option, } impl From for AgentProviderResponse { @@ -220,10 +250,22 @@ impl From<&AgentProviderBinding> for AgentProviderResponse { .map(AgentProviderModelResponse::from) .collect(), source: provider.source.into(), + matched_inference_provider: None, } } } +impl AgentProviderResponse { + pub fn with_matched_inference_provider( + mut self, + provider: &InferenceProvider, + ) -> Self { + self.source_provider_id = Some(provider.id.clone()); + self.matched_inference_provider = Some(provider.into()); + self + } +} + #[derive(Debug, Deserialize, TS)] #[ts(export)] pub struct CreateAgentProviderRequest { @@ -233,5 +275,6 @@ pub struct CreateAgentProviderRequest { #[derive(Debug, Deserialize, TS)] #[ts(export)] pub struct UpdateAgentProviderRequest { - pub inference_provider_id: String, + pub name: Option, + pub api_key: Option, } diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 383faa12..4f6ecc57 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -170,6 +170,7 @@ pub async fn start(options: ApiOptions) -> Result<(), rocket::Error> { routes::inference::list_opencode_providers, routes::inference::create_opencode_provider, routes::inference::update_opencode_provider, + routes::inference::sync_opencode_provider, routes::inference::delete_opencode_provider, routes::inference::get_inference_provider_password, routes::inference::create_inference_provider, diff --git a/crates/api/src/routes/inference.rs b/crates/api/src/routes/inference.rs index 1b5ed910..34b2f594 100644 --- a/crates/api/src/routes/inference.rs +++ b/crates/api/src/routes/inference.rs @@ -1,6 +1,7 @@ use aghub_inference::{ - AgentProviderAdapter, InferenceProvider, InferenceProviderRepository, - InferenceProviderStore, OpenCodeProviderAdapter, + AgentProviderAdapter, AgentProviderBinding, InferenceProvider, + InferenceProviderRepository, InferenceProviderStore, + OpenCodeProviderAdapter, }; use rocket::http::Status; use rocket::response::status::NoContent; @@ -55,7 +56,7 @@ fn get_inventory_provider( Status::UnprocessableEntity, format!( "inference provider '{}' has no stored API key", - provider.name + provider.display_name ), "MISSING_CREDENTIAL", ) @@ -63,6 +64,67 @@ fn get_inventory_provider( Ok((provider, api_key)) } +fn same_api_base_url(left: &str, right: &str) -> bool { + left.trim().trim_end_matches('/') == right.trim().trim_end_matches('/') +} + +fn inventory_providers_with_api_keys( + store: &InferenceProviderStore, +) -> Result, ApiError> { + let mut providers = Vec::new(); + for provider in store.list().map_err(ApiError::from)? { + let Some(api_key) = + store.get_api_key(&provider.id).map_err(ApiError::from)? + else { + continue; + }; + providers.push((provider, api_key)); + } + Ok(providers) +} + +fn find_matching_inventory_provider( + inventory: &[(InferenceProvider, String)], + adapter: &OpenCodeProviderAdapter, + binding: &AgentProviderBinding, +) -> Result, ApiError> { + let Some(api_base_url) = binding.api_base_url.as_deref() else { + return Ok(None); + }; + let Some(agent_api_key) = + adapter.api_key(&binding.id).map_err(ApiError::from)? + else { + return Ok(None); + }; + + for (provider, api_key) in inventory { + if !same_api_base_url(&provider.api_base_url, api_base_url) { + continue; + } + if api_key == &agent_api_key { + return Ok(Some((provider.clone(), api_key.clone()))); + } + } + + Ok(None) +} + +fn opencode_provider_response( + inventory: &[(InferenceProvider, String)], + adapter: &OpenCodeProviderAdapter, + binding: AgentProviderBinding, +) -> Result { + let matched = + find_matching_inventory_provider(inventory, adapter, &binding)?; + let response = AgentProviderResponse::from(binding); + Ok(match matched { + Some((provider, _)) => { + response.with_matched_inference_provider(&provider) + } + None => response, + }) +} + #[get("/inference/providers")] pub fn list_inference_providers( state: &State, @@ -77,14 +139,21 @@ pub fn list_inference_providers( } #[get("/inference/agents/opencode/providers")] -pub fn list_opencode_providers() -> ApiResult> { - let providers = opencode_adapter()? +pub fn list_opencode_providers( + state: &State, +) -> ApiResult> { + let store = store(state); + let adapter = opencode_adapter()?; + let inventory = inventory_providers_with_api_keys(&store)?; + let providers = adapter .load_providers() .map_err(ApiError::from)? .providers .into_iter() - .map(AgentProviderResponse::from) - .collect(); + .map(|binding| { + opencode_provider_response(&inventory, &adapter, binding) + }) + .collect::, _>>()?; Ok(Json(providers)) } @@ -105,33 +174,59 @@ pub fn create_opencode_provider( #[put("/inference/agents/opencode/providers/", data = "")] pub fn update_opencode_provider( - state: &State, id: &str, body: Json, +) -> ApiResult { + let body = body.into_inner(); + let binding = opencode_adapter()? + .update_provider(id, body.name.as_deref(), body.api_key.as_deref()) + .map_err(ApiError::from)?; + + Ok(Json(binding.into())) +} + +#[post("/inference/agents/opencode/providers//sync")] +pub fn sync_opencode_provider( + state: &State, + id: &str, ) -> ApiResult { let store = store(state); - let (provider, api_key) = - get_inventory_provider(&store, &body.inference_provider_id)?; let adapter = opencode_adapter()?; - let exists = adapter + let inventory = inventory_providers_with_api_keys(&store)?; + let binding = adapter .load_providers() .map_err(ApiError::from)? .providers - .iter() - .any(|provider| provider.id == id); - if !exists { + .into_iter() + .find(|provider| provider.id == id) + .ok_or_else(|| { + ApiError::new( + Status::NotFound, + format!("OpenCode provider '{id}' not found"), + "RESOURCE_NOT_FOUND", + ) + })?; + let Some((provider, api_key)) = + find_matching_inventory_provider(&inventory, &adapter, &binding)? + else { return Err(ApiError::new( - Status::NotFound, - format!("OpenCode provider '{id}' not found"), - "RESOURCE_NOT_FOUND", + Status::UnprocessableEntity, + format!( + "OpenCode provider '{id}' is not backed by an aghub \ + inference provider" + ), + "UNRECOGNIZED_PROVIDER", )); - } + }; - let binding = adapter + let updated = adapter .add_provider(id, &provider, &api_key) .map_err(ApiError::from)?; - Ok(Json(binding.into())) + Ok(Json( + AgentProviderResponse::from(updated) + .with_matched_inference_provider(&provider), + )) } #[delete("/inference/agents/opencode/providers/")] @@ -157,7 +252,7 @@ pub fn get_inference_provider_password( Status::NotFound, format!( "inference provider '{}' has no stored API key", - provider.name + provider.display_name ), "RESOURCE_NOT_FOUND", ) diff --git a/crates/desktop/src/generated/dto/AgentProviderMatchedInferenceProviderResponse.ts b/crates/desktop/src/generated/dto/AgentProviderMatchedInferenceProviderResponse.ts new file mode 100644 index 00000000..1e189aed --- /dev/null +++ b/crates/desktop/src/generated/dto/AgentProviderMatchedInferenceProviderResponse.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AgentProviderMatchedInferenceProviderResponse = { + id: string; + name: string; + display_name: string; + model_count: number; +}; diff --git a/crates/desktop/src/generated/dto/AgentProviderResponse.ts b/crates/desktop/src/generated/dto/AgentProviderResponse.ts index 183a221b..7dd72521 100644 --- a/crates/desktop/src/generated/dto/AgentProviderResponse.ts +++ b/crates/desktop/src/generated/dto/AgentProviderResponse.ts @@ -1,5 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AgentProviderCredentialDto } from "./AgentProviderCredentialDto"; +import type { AgentProviderMatchedInferenceProviderResponse } from "./AgentProviderMatchedInferenceProviderResponse"; import type { AgentProviderModelResponse } from "./AgentProviderModelResponse"; import type { AgentProviderSourceDto } from "./AgentProviderSourceDto"; import type { InferenceProviderFormatDto } from "./InferenceProviderFormatDto"; @@ -13,4 +14,5 @@ export type AgentProviderResponse = { credential: AgentProviderCredentialDto; models: Array; source: AgentProviderSourceDto; + matched_inference_provider: AgentProviderMatchedInferenceProviderResponse | null; }; diff --git a/crates/desktop/src/generated/dto/CreateInferenceProviderRequest.ts b/crates/desktop/src/generated/dto/CreateInferenceProviderRequest.ts index a9fbea32..74d42ce2 100644 --- a/crates/desktop/src/generated/dto/CreateInferenceProviderRequest.ts +++ b/crates/desktop/src/generated/dto/CreateInferenceProviderRequest.ts @@ -3,6 +3,7 @@ import type { InferenceProviderFormatDto } from "./InferenceProviderFormatDto"; export type CreateInferenceProviderRequest = { name: string; + display_name: string; format: InferenceProviderFormatDto; api_base_url: string; api_key: string; diff --git a/crates/desktop/src/generated/dto/InferenceProviderResponse.ts b/crates/desktop/src/generated/dto/InferenceProviderResponse.ts index 9dbb0365..a60ed6cb 100644 --- a/crates/desktop/src/generated/dto/InferenceProviderResponse.ts +++ b/crates/desktop/src/generated/dto/InferenceProviderResponse.ts @@ -4,6 +4,7 @@ import type { InferenceProviderFormatDto } from "./InferenceProviderFormatDto"; export type InferenceProviderResponse = { id: string; name: string; + display_name: string; format: InferenceProviderFormatDto; api_base_url: string; masked_api_key: string; diff --git a/crates/desktop/src/generated/dto/UpdateAgentProviderRequest.ts b/crates/desktop/src/generated/dto/UpdateAgentProviderRequest.ts index 7c6a3a76..92833e01 100644 --- a/crates/desktop/src/generated/dto/UpdateAgentProviderRequest.ts +++ b/crates/desktop/src/generated/dto/UpdateAgentProviderRequest.ts @@ -1,3 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type UpdateAgentProviderRequest = { inference_provider_id: string }; +export type UpdateAgentProviderRequest = { + name: string | null; + api_key: string | null; +}; diff --git a/crates/desktop/src/generated/dto/UpdateInferenceProviderRequest.ts b/crates/desktop/src/generated/dto/UpdateInferenceProviderRequest.ts index a00d5387..4ea44ebf 100644 --- a/crates/desktop/src/generated/dto/UpdateInferenceProviderRequest.ts +++ b/crates/desktop/src/generated/dto/UpdateInferenceProviderRequest.ts @@ -3,6 +3,7 @@ import type { InferenceProviderFormatDto } from "./InferenceProviderFormatDto"; export type UpdateInferenceProviderRequest = { name: string | null; + display_name: string | null; format: InferenceProviderFormatDto | null; api_base_url: string | null; api_key: string | null; diff --git a/crates/desktop/src/generated/dto/index.ts b/crates/desktop/src/generated/dto/index.ts index dda3b552..41879278 100644 --- a/crates/desktop/src/generated/dto/index.ts +++ b/crates/desktop/src/generated/dto/index.ts @@ -1,6 +1,7 @@ export type { AgentAvailabilityDto } from "./AgentAvailabilityDto"; export type { AgentInfo } from "./AgentInfo"; export type { AgentProviderCredentialDto } from "./AgentProviderCredentialDto"; +export type { AgentProviderMatchedInferenceProviderResponse } from "./AgentProviderMatchedInferenceProviderResponse"; export type { AgentProviderModelResponse } from "./AgentProviderModelResponse"; export type { AgentProviderResponse } from "./AgentProviderResponse"; export type { AgentProviderSourceDto } from "./AgentProviderSourceDto"; diff --git a/crates/desktop/src/lib/api.ts b/crates/desktop/src/lib/api.ts index cbc88bbb..54379ef3 100644 --- a/crates/desktop/src/lib/api.ts +++ b/crates/desktop/src/lib/api.ts @@ -91,6 +91,7 @@ export function createApi(baseUrl: string) { .put(`inference/providers/${encodeURIComponent(provider.name)}`, { json: { name: null, + display_name: null, format: null, api_base_url: null, api_key: null, @@ -532,6 +533,13 @@ export function createApi(baseUrl: string) { ) .json(); }, + syncOpenCode(id: string): Promise { + return client + .post( + `inference/agents/opencode/providers/${encodeURIComponent(id)}/sync`, + ) + .json(); + }, async createModel( providerName: string, modelName: string, diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index 8c15f7ac..9b709bab 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -109,6 +109,14 @@ export default { openCodeProviderUpdated: "OpenCode provider updated", openCodeProviderDeleted: "OpenCode provider deleted", openCodeProviderDeleteError: "Failed to delete OpenCode provider", + openCodeProviderSynced: "OpenCode provider updated", + openCodeProviderSyncError: "Failed to update OpenCode provider", + openCodeBuiltInProvider: "Built-in provider", + openCodeBuiltInProviderInfo: + "No Aghub inference provider has the same API Base URL and API key. This was likely added directly in OpenCode.", + syncOpenCodeProvider: "Update OpenCode provider", + syncOpenCodeProviderFromInferenceProvider: + "Update models and config from {{name}}", agentProviderSourceCustom: "Custom", agentProviderSourceBuiltIn: "Built-in", agentProviderSourceClosedSlot: "Closed slot", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index 441a544d..45b844c0 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -105,6 +105,13 @@ export default { openCodeProviderUpdated: "OpenCode Provider 已更新", openCodeProviderDeleted: "OpenCode Provider 已删除", openCodeProviderDeleteError: "删除 OpenCode Provider 失败", + openCodeProviderSynced: "OpenCode Provider 已更新", + openCodeProviderSyncError: "更新 OpenCode Provider 失败", + openCodeBuiltInProvider: "内置 provider", + openCodeBuiltInProviderInfo: + "未在 Aghub 推理 Provider 中找到相同 API Base URL 和 API key,疑似用户直接在 OpenCode 里添加。", + syncOpenCodeProvider: "更新 OpenCode Provider", + syncOpenCodeProviderFromInferenceProvider: "从 {{name}} 更新模型列表和配置", agentProviderSourceCustom: "自定义", agentProviderSourceBuiltIn: "内置", agentProviderSourceClosedSlot: "封闭槽位", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index d921d16b..bba27db2 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -105,6 +105,13 @@ export default { openCodeProviderUpdated: "OpenCode Provider 已更新", openCodeProviderDeleted: "OpenCode Provider 已刪除", openCodeProviderDeleteError: "刪除 OpenCode Provider 失敗", + openCodeProviderSynced: "OpenCode Provider 已更新", + openCodeProviderSyncError: "更新 OpenCode Provider 失敗", + openCodeBuiltInProvider: "內建 provider", + openCodeBuiltInProviderInfo: + "未在 Aghub 推理 Provider 中找到相同 API Base URL 和 API key,疑似使用者直接在 OpenCode 裡新增。", + syncOpenCodeProvider: "更新 OpenCode Provider", + syncOpenCodeProviderFromInferenceProvider: "從 {{name}} 更新模型列表和配置", agentProviderSourceCustom: "自訂", agentProviderSourceBuiltIn: "內建", agentProviderSourceClosedSlot: "封閉槽位", diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index 71fbe04b..e97cb7fa 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -100,7 +100,7 @@ const CODING_AGENT_OPTIONS: CodingAgentOption[] = [ ]; interface InferenceProviderFormValues { - name: string; + displayName: string; format: InferenceProviderFormatDto; apiBaseUrl: string; apiKey: string; @@ -284,7 +284,7 @@ function ProviderForm({ mode: "onSubmit", reValidateMode: "onChange", defaultValues: { - name: provider?.name ?? "", + displayName: provider?.display_name ?? "", format: provider?.format ?? "openai_responses", apiBaseUrl: provider?.api_base_url ?? "", apiKey: "", @@ -311,7 +311,7 @@ function ProviderForm({ createMutation.isPending || updateMutation.isPending || isSubmitting; const onSubmit = async (values: InferenceProviderFormValues) => { - const name = values.name.trim(); + const displayName = values.displayName.trim(); const apiBaseUrl = values.apiBaseUrl.trim(); const apiKey = values.apiKey.trim(); const models = normalizeModelNames(values.models); @@ -319,7 +319,8 @@ function ProviderForm({ try { if (mode === "create") { const created = await createMutation.mutateAsync({ - name, + name: displayName, + display_name: displayName, format: values.format, api_base_url: apiBaseUrl, api_key: apiKey, @@ -334,7 +335,8 @@ function ProviderForm({ const updated = await updateMutation.mutateAsync({ name: provider.name, body: { - name, + name: null, + display_name: displayName, format: values.format, api_base_url: apiBaseUrl, api_key: apiKey || null, @@ -381,7 +383,7 @@ function ProviderForm({

- {provider.name} + {provider.display_name}

@@ -876,7 +878,7 @@ function ProviderDetail({ {t("deleteInferenceProviderConfirm", { - name: provider.name, + name: provider.display_name, })} @@ -943,8 +945,9 @@ export default function InferenceProvidersPage() { () => new Fuse(providers, { keys: [ - { name: "name", weight: 2 }, + { name: "display_name", weight: 2 }, { name: "models", weight: 2 }, + { name: "name", weight: 1 }, { name: "api_base_url", weight: 1 }, { name: "format", weight: 1 }, ], @@ -1127,7 +1130,7 @@ export default function InferenceProvidersPage() {
@@ -1136,7 +1139,7 @@ export default function InferenceProvidersPage() { />
diff --git a/crates/desktop/src/pages/inference-providers/opencode-panel.tsx b/crates/desktop/src/pages/inference-providers/opencode-panel.tsx index 4ae2adcf..06251e1e 100644 --- a/crates/desktop/src/pages/inference-providers/opencode-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/opencode-panel.tsx @@ -2,6 +2,7 @@ import { ArrowPathIcon, PencilIcon, PlusIcon, + QuestionMarkCircleIcon, ServerIcon, TrashIcon, } from "@heroicons/react/24/solid"; @@ -10,21 +11,23 @@ import { AlertDialog, Button, Card, + FieldError, + Input, Label, ListBox, + Modal, Select, Spinner, + TextField, Tooltip, toast, } from "@heroui/react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import type { FormEvent } from "react"; import { useTranslation } from "react-i18next"; import type { AgentProviderResponse, - AgentProviderSourceDto, - InferenceProviderFormatDto, InferenceProviderResponse, } from "../../generated/dto"; import { useApi } from "../../hooks/use-api"; @@ -35,99 +38,35 @@ import { deleteOpenCodeProviderMutationOptions, inferenceProviderListQueryOptions, openCodeProviderListQueryOptions, + syncOpenCodeProviderMutationOptions, updateOpenCodeProviderMutationOptions, } from "../../requests/inference-providers"; -type PanelMode = - | { type: "list" } +type ProviderDialogMode = | { type: "create" } | { type: "edit"; provider: AgentProviderResponse }; -function formatLabelKey(format: InferenceProviderFormatDto | null) { - switch (format) { - case "anthropic": - return "inferenceFormatAnthropic"; - case "openai_completions": - return "inferenceFormatOpenAiCompletions"; - case "openai_responses": - return "inferenceFormatOpenAiResponses"; - default: - return "unknown"; - } -} - -function sourceLabelKey(source: AgentProviderSourceDto) { - switch (source) { - case "built_in": - return "agentProviderSourceBuiltIn"; - case "closed_slot": - return "agentProviderSourceClosedSlot"; - case "custom": - return "agentProviderSourceCustom"; - case "stored_credential": - return "agentProviderSourceStoredCredential"; - } -} - -function credentialLabel(provider: AgentProviderResponse) { - switch (provider.credential.type) { - case "agent_store": - return "agentProviderCredentialAgentStore"; - case "env_var": - return provider.credential.name; - case "inline": - return "agentProviderCredentialInline"; - case "none": - return "agentProviderCredentialNone"; - } -} - -function findInventoryProviderId( - provider: AgentProviderResponse | undefined, - inventoryProviders: InferenceProviderResponse[], -) { - if (!provider) return inventoryProviders[0]?.id ?? ""; - if ( - provider.source_provider_id && - inventoryProviders.some( - (item) => item.id === provider.source_provider_id, - ) - ) { - return provider.source_provider_id; - } - - return ( - inventoryProviders.find( - (item) => - item.name === provider.name && - item.api_base_url === provider.api_base_url && - item.format === provider.format, - )?.id ?? - inventoryProviders.find((item) => item.name === provider.name)?.id ?? - inventoryProviders[0]?.id ?? - "" - ); -} - -function OpenCodeProviderForm({ - mode, - provider, +function OpenCodeCreateProviderDialog({ + isOpen, inventoryProviders, - onCancel, - onSaved, + isInventoryLoading, + onClose, }: { - mode: "create" | "edit"; - provider?: AgentProviderResponse; + isOpen: boolean; inventoryProviders: InferenceProviderResponse[]; - onCancel: () => void; - onSaved: () => void; + isInventoryLoading: boolean; + onClose: () => void; }) { const { t } = useTranslation(); const api = useApi(); const queryClient = useQueryClient(); - const [selectedProviderId, setSelectedProviderId] = useState(() => - findInventoryProviderId(provider, inventoryProviders), - ); + const [selectedProviderId, setSelectedProviderId] = useState(""); + + const defaultProviderId = inventoryProviders[0]?.id ?? ""; + useEffect(() => { + if (!isOpen) return; + setSelectedProviderId((current) => current || defaultProviderId); + }, [defaultProviderId, isOpen]); const createMutation = useMutation({ ...createOpenCodeProviderMutationOptions({ @@ -135,141 +74,301 @@ function OpenCodeProviderForm({ queryClient, onSuccess: async () => { toast.success(t("openCodeProviderCreated")); - onSaved(); + onClose(); }, }), }); + + const activeError = createMutation.error; + const isPending = createMutation.isPending; + const hasInventoryProviders = inventoryProviders.length > 0; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + if (!selectedProviderId) return; + + createMutation.mutate({ + inference_provider_id: selectedProviderId, + }); + }; + + return ( + { + if (!open) onClose(); + }} + > + + + + + + {t("createOpenCodeProvider")} + + +
+ + {activeError && ( + + + + + {activeError instanceof Error + ? activeError.message + : String(activeError)} + + + + )} + + {isInventoryLoading && ( +
+ +
+ )} + + {!isInventoryLoading && !hasInventoryProviders && ( + + + + + {t( + "noInferenceProvidersForOpenCode", + )} + + + + )} + + {!isInventoryLoading && hasInventoryProviders && ( + + )} +
+ + + + +
+
+
+
+ ); +} + +function OpenCodeEditProviderDialog({ + isOpen, + provider, + onClose, +}: { + isOpen: boolean; + provider: AgentProviderResponse; + onClose: () => void; +}) { + const { t } = useTranslation(); + const api = useApi(); + const queryClient = useQueryClient(); + const [name, setName] = useState(provider.name); + const [apiKey, setApiKey] = useState(""); + const [nameError, setNameError] = useState(null); + + useEffect(() => { + if (!isOpen) return; + setName(provider.name); + setApiKey(""); + setNameError(null); + }, [isOpen, provider.id, provider.name]); + const updateMutation = useMutation({ ...updateOpenCodeProviderMutationOptions({ api, queryClient, onSuccess: async () => { toast.success(t("openCodeProviderUpdated")); - onSaved(); + onClose(); }, }), }); - const activeError = - mode === "create" ? createMutation.error : updateMutation.error; - const isPending = createMutation.isPending || updateMutation.isPending; - const hasInventoryProviders = inventoryProviders.length > 0; - const handleSubmit = (event: FormEvent) => { event.preventDefault(); - if (!selectedProviderId) return; - - if (mode === "create") { - createMutation.mutate({ - inference_provider_id: selectedProviderId, - }); + const trimmedName = name.trim(); + if (!trimmedName) { + setNameError(t("validationProviderNameRequired")); return; } - if (!provider) return; + const trimmedApiKey = apiKey.trim(); updateMutation.mutate({ id: provider.id, body: { - inference_provider_id: selectedProviderId, + name: trimmedName === provider.name ? null : trimmedName, + api_key: trimmedApiKey ? trimmedApiKey : null, }, }); }; return ( -
- {activeError && ( - - - - - {activeError instanceof Error - ? activeError.message - : String(activeError)} - - - - )} - - {!hasInventoryProviders && ( - - - - - {t("noInferenceProvidersForOpenCode")} - - - - )} + { + if (!open) onClose(); + }} + > + + + + + + {t("editOpenCodeProvider")} + + + + + {updateMutation.error && ( + + + + + {updateMutation.error instanceof + Error + ? updateMutation.error.message + : String(updateMutation.error)} + + + + )} - + + { + setName(event.target.value); + if (nameError) setNameError(null); + }} + placeholder={t("providerNamePlaceholder")} + variant="secondary" + /> + {nameError && ( + {nameError} + )} + -
- - -
- + + + + setApiKey(event.target.value) + } + placeholder={t( + "providerApiKeyEditPlaceholder", + )} + variant="secondary" + /> + +
+ + + + + +
+
+
); } function ProviderRow({ provider, + isSyncing, onEdit, + onSync, onDelete, }: { provider: AgentProviderResponse; + isSyncing: boolean; onEdit: () => void; + onSync: () => void; onDelete: () => void; }) { const { t } = useTranslation(); - const credential = credentialLabel(provider); + const matchedProvider = provider.matched_inference_provider; return (
@@ -284,25 +383,56 @@ function ProviderRow({ )}
- {t(formatLabelKey(provider.format))} - {provider.api_base_url && ( - - {provider.api_base_url} + {matchedProvider ? ( + + {t("providerModels")}: {matchedProvider.model_count} + + ) : ( + + {t("openCodeBuiltInProvider")} + + + + + + + + {t("openCodeBuiltInProviderInfo")} + + )} - - {t("providerModels")}: {provider.models.length} - - {t(sourceLabelKey(provider.source))} - - {credential.startsWith("agentProvider") - ? t(credential) - : credential} -
+ {matchedProvider && ( + + + + + + {t("syncOpenCodeProviderFromInferenceProvider", { + name: matchedProvider.display_name, + })} + + + )}
+ setProviderDialog(null)} + /> + {providerDialog?.type === "edit" && ( + setProviderDialog(null)} + /> + )} + { diff --git a/crates/desktop/src/requests/inference-providers.ts b/crates/desktop/src/requests/inference-providers.ts index 0df18081..b433cc7a 100644 --- a/crates/desktop/src/requests/inference-providers.ts +++ b/crates/desktop/src/requests/inference-providers.ts @@ -194,6 +194,29 @@ export function updateOpenCodeProviderMutationOptions({ }); } +interface SyncAgentProviderMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: ( + data: AgentProviderResponse, + id: string, + ) => void | Promise; +} + +export function syncOpenCodeProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: SyncAgentProviderMutationParams) { + return mutationOptions({ + mutationFn: (id: string) => api.inferenceProviders.syncOpenCode(id), + onSuccess: async (data, id) => { + await invalidateOpenCodeProviderQueries(queryClient); + await onSuccess?.(data, id); + }, + }); +} + interface UpdateInferenceModelVariables { providerName: string; modelName: string; diff --git a/crates/inference/migrations/0004_add_display_name.sql b/crates/inference/migrations/0004_add_display_name.sql new file mode 100644 index 00000000..e6a1d939 --- /dev/null +++ b/crates/inference/migrations/0004_add_display_name.sql @@ -0,0 +1,6 @@ +ALTER TABLE inference_providers +ADD COLUMN display_name TEXT NOT NULL DEFAULT ''; + +UPDATE inference_providers +SET display_name = name +WHERE display_name = ''; diff --git a/crates/inference/src/agent.rs b/crates/inference/src/agent.rs index 7156ab4f..79252b02 100644 --- a/crates/inference/src/agent.rs +++ b/crates/inference/src/agent.rs @@ -413,7 +413,7 @@ impl AgentProviderBinding { Ok(Self { id, source_provider_id: Some(provider.id.clone()), - name: provider.name.clone(), + name: provider.display_name.clone(), format: Some(provider.format), api_base_url: Some(provider.api_base_url.clone()), credential, @@ -626,7 +626,8 @@ mod tests { fn provider() -> InferenceProvider { InferenceProvider { id: "inventory-id".to_string(), - name: "OpenRouter".to_string(), + name: "openrouter".to_string(), + display_name: "OpenRouter".to_string(), format: InferenceProviderFormat::OpenAiResponses, api_base_url: "https://openrouter.ai/api/v1".to_string(), masked_api_key: "sk****st".to_string(), @@ -691,6 +692,7 @@ mod tests { .unwrap(); assert_eq!(binding.id, "openrouter"); + assert_eq!(binding.name, "OpenRouter"); assert_eq!(binding.source_provider_id.as_deref(), Some("inventory-id")); assert_eq!(binding.models.len(), 2); } diff --git a/crates/inference/src/model.rs b/crates/inference/src/model.rs index a022857d..b4f628b7 100644 --- a/crates/inference/src/model.rs +++ b/crates/inference/src/model.rs @@ -68,9 +68,12 @@ pub struct InferenceProvider { /// Stable provider identifier. pub id: String, - /// User-visible provider name. + /// Stable provider key. pub name: String, + /// User-visible provider name. + pub display_name: String, + /// Request/response format supported by this provider. pub format: InferenceProviderFormat, @@ -87,9 +90,12 @@ pub struct InferenceProvider { /// Provider creation input. #[derive(Clone, Deserialize)] pub struct CreateInferenceProvider { - /// User-visible provider name. + /// Stable provider key. pub name: String, + /// User-visible provider name. + pub display_name: String, + /// Request/response format supported by this provider. pub format: InferenceProviderFormat, @@ -108,6 +114,7 @@ impl fmt::Debug for CreateInferenceProvider { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("CreateInferenceProvider") .field("name", &self.name) + .field("display_name", &self.display_name) .field("format", &self.format) .field("api_base_url", &self.api_base_url) .field("models", &self.models) @@ -119,9 +126,12 @@ impl fmt::Debug for CreateInferenceProvider { /// Provider update input. #[derive(Clone, Default, Deserialize)] pub struct UpdateInferenceProvider { - /// Updated user-visible provider name. + /// Updated stable provider key. pub name: Option, + /// Updated user-visible provider name. + pub display_name: Option, + /// Updated request/response format. pub format: Option, @@ -139,6 +149,7 @@ impl fmt::Debug for UpdateInferenceProvider { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("UpdateInferenceProvider") .field("name", &self.name) + .field("display_name", &self.display_name) .field("format", &self.format) .field("api_base_url", &self.api_base_url) .field("models", &self.models) diff --git a/crates/inference/src/opencode/mapping.rs b/crates/inference/src/opencode/mapping.rs index 6d6ada3d..9a727323 100644 --- a/crates/inference/src/opencode/mapping.rs +++ b/crates/inference/src/opencode/mapping.rs @@ -183,6 +183,15 @@ pub(super) fn clean_provider_id(provider_id: &str) -> Result { } } +pub(super) fn clean_provider_name(name: &str) -> Result { + let name = name.trim().to_string(); + if name.is_empty() { + Err(InferenceProviderError::EmptyName) + } else { + Ok(name) + } +} + pub(super) fn ensure_api_key(api_key: &str) -> Result<()> { if api_key.trim().is_empty() { Err(InferenceProviderError::EmptyApiKey) diff --git a/crates/inference/src/opencode/mod.rs b/crates/inference/src/opencode/mod.rs index b2f1c8b8..1b87f62a 100644 --- a/crates/inference/src/opencode/mod.rs +++ b/crates/inference/src/opencode/mod.rs @@ -14,7 +14,7 @@ mod tests; use std::collections::HashSet; use std::path::{Path, PathBuf}; -use serde_json::json; +use serde_json::{json, Value}; use crate::agent::{ AgentCredentialSupport, AgentProviderAdapter, AgentProviderBinding, @@ -103,7 +103,7 @@ impl OpenCodeProviderAdapter { Ok(binding) } - /// Add a provider using a slug derived from its display name. + /// Add a provider using a slug derived from its stable key. pub fn add_inventory_provider( &self, provider: &InferenceProvider, @@ -113,6 +113,76 @@ impl OpenCodeProviderAdapter { self.add_provider(&provider_id, provider, api_key) } + /// Update an existing OpenCode provider's display name and/or API key. + pub fn update_provider( + &self, + provider_id: &str, + name: Option<&str>, + api_key: Option<&str>, + ) -> Result { + let provider_id = mapping::clean_provider_id(provider_id)?; + let current = self + .load_providers()? + .providers + .into_iter() + .find(|provider| provider.id == provider_id) + .ok_or_else(|| { + crate::error::InferenceProviderError::NotFound( + provider_id.clone(), + ) + })?; + + let name = name.map(mapping::clean_provider_name).transpose()?; + if let Some(api_key) = api_key { + mapping::ensure_api_key(api_key)?; + } + + if let Some(name) = name { + let mut config = files::read_config(&self.config_path)?; + config.provider.entry(provider_id.clone()).or_default().name = + Some(name); + files::write_config(&self.config_path, config)?; + } + + if let Some(api_key) = api_key { + self.set_api_auth(&provider_id, api_key)?; + } + + Ok(self + .load_providers()? + .providers + .into_iter() + .find(|provider| provider.id == provider_id) + .unwrap_or(current)) + } + + /// Read an API key visible to OpenCode for a provider. + pub fn api_key(&self, provider_id: &str) -> Result> { + let provider_id = mapping::clean_provider_id(provider_id)?; + let auth = files::read_auth_values(&self.auth_path)?; + if let Some(api_key) = + auth.get(&provider_id).and_then(auth_entry_api_key) + { + return Ok(Some(api_key.to_string())); + } + + let config = files::read_config(&self.config_path)?; + let Some(api_key) = config + .provider + .get(&provider_id) + .and_then(|provider| provider.options.get("apiKey")) + .and_then(Value::as_str) + else { + return Ok(None); + }; + + if let Some(env_name) = parse_env_var_ref(api_key) { + return Ok(std::env::var(env_name).ok()); + } + + Ok(Some(api_key.to_string())) + } + /// Remove a provider definition and matching OpenCode auth entry. pub fn remove_provider( &self, @@ -157,6 +227,23 @@ impl OpenCodeProviderAdapter { } } +fn auth_entry_api_key(entry: &Value) -> Option<&str> { + if entry.get("type").and_then(Value::as_str) == Some("api") { + entry.get("key").and_then(Value::as_str) + } else { + None + } +} + +fn parse_env_var_ref(value: &str) -> Option<&str> { + let name = value.strip_prefix("{env:")?.strip_suffix('}')?.trim(); + if name.is_empty() { + None + } else { + Some(name) + } +} + impl AgentProviderAdapter for OpenCodeProviderAdapter { fn agent_id(&self) -> &'static str { AGENT_ID diff --git a/crates/inference/src/opencode/tests.rs b/crates/inference/src/opencode/tests.rs index 61dec77c..7e4d009b 100644 --- a/crates/inference/src/opencode/tests.rs +++ b/crates/inference/src/opencode/tests.rs @@ -17,6 +17,7 @@ fn provider() -> InferenceProvider { InferenceProvider { id: "inventory-id".to_string(), name: "OpenRouter".to_string(), + display_name: "OpenRouter".to_string(), format: InferenceProviderFormat::OpenAiCompletions, api_base_url: "https://openrouter.ai/api/v1".to_string(), masked_api_key: "sk****st".to_string(), @@ -202,6 +203,107 @@ fn add_provider_preserves_jsonc_comments() { assert_eq!(auth.unwrap()["openrouter"]["key"], "sk-test"); } +#[test] +fn update_provider_edits_name_and_auth_key() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + adapter + .add_provider("openrouter", &provider(), "sk-old") + .unwrap(); + + let binding = adapter + .update_provider("openrouter", Some("OpenRouter Team"), Some("sk-new")) + .unwrap(); + + assert_eq!(binding.name, "OpenRouter Team"); + let config: Value = serde_json::from_str( + &fs::read_to_string(adapter.config_path()).unwrap(), + ) + .unwrap(); + assert_eq!(config["provider"]["openrouter"]["name"], "OpenRouter Team"); + assert_eq!( + config["provider"]["openrouter"]["options"]["baseURL"], + "https://openrouter.ai/api/v1" + ); + + let auth: Value = + serde_json::from_str(&fs::read_to_string(adapter.auth_path()).unwrap()) + .unwrap(); + assert_eq!(auth["openrouter"]["key"], "sk-new"); +} + +#[test] +fn update_provider_can_name_auth_only_provider() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.auth_path(), + r#"{ "auth-only": { "type": "api", "key": "old" } }"#, + ) + .unwrap(); + + let binding = adapter + .update_provider("auth-only", Some("Auth Only"), None) + .unwrap(); + + assert_eq!(binding.name, "Auth Only"); + assert_eq!(binding.source, AgentProviderSource::Custom); + let config: Value = serde_json::from_str( + &fs::read_to_string(adapter.config_path()).unwrap(), + ) + .unwrap(); + assert_eq!(config["provider"]["auth-only"]["name"], "Auth Only"); +} + +#[test] +fn api_key_reads_auth_store_before_config() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#"{ + "provider": { + "local": { + "options": { "apiKey": "inline-key" } + } + } + }"#, + ) + .unwrap(); + fs::write( + adapter.auth_path(), + r#"{ "local": { "type": "api", "key": "auth-key" } }"#, + ) + .unwrap(); + + assert_eq!( + adapter.api_key("local").unwrap().as_deref(), + Some("auth-key") + ); +} + +#[test] +fn api_key_reads_inline_config_key() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#"{ + "provider": { + "local": { + "options": { "apiKey": "inline-key" } + } + } + }"#, + ) + .unwrap(); + + assert_eq!( + adapter.api_key("local").unwrap().as_deref(), + Some("inline-key") + ); +} + #[test] fn save_preserves_auth_only_entries_without_configuring_them() { let temp = tempfile::tempdir().unwrap(); diff --git a/crates/inference/src/store.rs b/crates/inference/src/store.rs index 0d511c8c..6d5452b5 100644 --- a/crates/inference/src/store.rs +++ b/crates/inference/src/store.rs @@ -143,7 +143,8 @@ impl InferenceProviderStore { id: &str, ) -> Result { let row = sqlx::query( - "SELECT id, name, format, api_base_url, masked_api_key \ + "SELECT id, name, display_name, format, api_base_url, \ + masked_api_key \ FROM inference_providers WHERE id = ?", ) .bind(id) @@ -237,6 +238,7 @@ fn map_row(row: sqlx::sqlite::SqliteRow) -> Result { Ok(InferenceProvider { id: row.try_get("id")?, name: row.try_get("name")?, + display_name: row.try_get("display_name")?, format, api_base_url: row.try_get("api_base_url")?, masked_api_key: row.try_get("masked_api_key")?, @@ -251,7 +253,8 @@ impl InferenceProviderRepository self.block_on(async { let mut conn = self.open_db().await?; let rows = sqlx::query( - "SELECT id, name, format, api_base_url, masked_api_key \ + "SELECT id, name, display_name, format, api_base_url, \ + masked_api_key \ FROM inference_providers ORDER BY rowid", ) .fetch_all(&mut conn) @@ -279,6 +282,7 @@ impl InferenceProviderRepository input: CreateInferenceProvider, ) -> Result { let name = clean_name(&input.name)?; + let display_name = clean_display_name(&input.display_name)?; let api_base_url = clean_api_base_url(&input.api_base_url)?; let models = clean_model_names(&input.models)?; ensure_api_key(&input.api_key)?; @@ -290,6 +294,7 @@ impl InferenceProviderRepository let provider = InferenceProvider { id: uuid::Uuid::new_v4().to_string(), name, + display_name, format: input.format, api_base_url, masked_api_key: mask_api_key(&input.api_key), @@ -301,11 +306,13 @@ impl InferenceProviderRepository let result: Result<()> = async { sqlx::query( "INSERT INTO inference_providers \ - (id, name, format, api_base_url, masked_api_key) \ - VALUES (?, ?, ?, ?, ?)", + (id, name, display_name, format, api_base_url, \ + masked_api_key) \ + VALUES (?, ?, ?, ?, ?, ?)", ) .bind(&provider.id) .bind(&provider.name) + .bind(&provider.display_name) .bind(provider.format.to_string()) .bind(&provider.api_base_url) .bind(&provider.masked_api_key) @@ -352,6 +359,10 @@ impl InferenceProviderRepository provider.name = name; } + if let Some(ref display_name) = input.display_name { + provider.display_name = clean_display_name(display_name)?; + } + if let Some(format) = input.format { provider.format = format; } @@ -378,11 +389,12 @@ impl InferenceProviderRepository let result: Result<()> = async { sqlx::query( "UPDATE inference_providers \ - SET name = ?, format = ?, api_base_url = ?, \ - masked_api_key = ? \ + SET name = ?, display_name = ?, format = ?, \ + api_base_url = ?, masked_api_key = ? \ WHERE id = ?", ) .bind(&provider.name) + .bind(&provider.display_name) .bind(provider.format.to_string()) .bind(&provider.api_base_url) .bind(&provider.masked_api_key) @@ -538,6 +550,15 @@ fn clean_name(name: &str) -> Result { } } +fn clean_display_name(display_name: &str) -> Result { + let display_name = display_name.trim(); + if display_name.is_empty() { + Err(InferenceProviderError::EmptyName) + } else { + Ok(display_name.to_string()) + } +} + fn ensure_api_key(api_key: &str) -> Result<()> { if api_key.trim().is_empty() { Err(InferenceProviderError::EmptyApiKey) @@ -623,6 +644,7 @@ mod tests { store .create(CreateInferenceProvider { name: name.to_string(), + display_name: name.to_string(), format: InferenceProviderFormat::OpenAiResponses, api_base_url: "https://api.openai.com/v1".to_string(), api_key: "secret".to_string(), @@ -645,6 +667,7 @@ mod tests { let provider = store .create(CreateInferenceProvider { name: "OpenAI".to_string(), + display_name: "OpenAI".to_string(), format: InferenceProviderFormat::OpenAiResponses, api_base_url: "https://api.openai.com/v1".to_string(), api_key: "sk-test".to_string(), @@ -655,6 +678,7 @@ mod tests { // Metadata is persisted and retrievable let fetched = store.get(&provider.id).unwrap(); assert_eq!(fetched.name, "OpenAI"); + assert_eq!(fetched.display_name, "OpenAI"); assert_eq!(fetched.format, InferenceProviderFormat::OpenAiResponses); assert_eq!(fetched.api_base_url, "https://api.openai.com/v1"); assert_eq!(fetched.masked_api_key, "s*****t"); @@ -689,6 +713,7 @@ mod tests { let provider = store .create(CreateInferenceProvider { name: "Anthropic".to_string(), + display_name: "Anthropic".to_string(), format: InferenceProviderFormat::Anthropic, api_base_url: "https://api.anthropic.com/v1".to_string(), api_key: "first-key".to_string(), @@ -701,6 +726,7 @@ mod tests { &provider.id, UpdateInferenceProvider { name: Some("Claude".to_string()), + display_name: Some("Claude Team".to_string()), format: Some(InferenceProviderFormat::OpenAiCompletions), api_base_url: Some( "https://gateway.example.com/v1".to_string(), @@ -712,6 +738,7 @@ mod tests { .unwrap(); assert_eq!(updated.name, "Claude"); + assert_eq!(updated.display_name, "Claude Team"); assert_eq!(updated.format, InferenceProviderFormat::OpenAiCompletions); assert_eq!(updated.api_base_url, "https://gateway.example.com/v1"); assert_eq!(updated.masked_api_key, "s********y"); @@ -727,6 +754,7 @@ mod tests { let provider = store .create(CreateInferenceProvider { name: "Anthropic".to_string(), + display_name: "Anthropic".to_string(), format: InferenceProviderFormat::Anthropic, api_base_url: "https://api.anthropic.com/v1".to_string(), api_key: "secret".to_string(), @@ -762,6 +790,7 @@ mod tests { store .create(CreateInferenceProvider { name: "OpenAI".to_string(), + display_name: "OpenAI".to_string(), format: InferenceProviderFormat::OpenAiResponses, api_base_url: "https://api.openai.com/v1".to_string(), api_key: "first".to_string(), @@ -772,6 +801,7 @@ mod tests { let error = store .create(CreateInferenceProvider { name: "openai".to_string(), + display_name: "OpenAI Gateway".to_string(), format: InferenceProviderFormat::OpenAiCompletions, api_base_url: "https://gateway.example.com/v1".to_string(), api_key: "second".to_string(), @@ -789,6 +819,7 @@ mod tests { let error = store .create(CreateInferenceProvider { name: "OpenAI".to_string(), + display_name: "OpenAI".to_string(), format: InferenceProviderFormat::OpenAiResponses, api_base_url: " ".to_string(), api_key: "secret".to_string(), @@ -805,6 +836,7 @@ mod tests { let provider = store .create(CreateInferenceProvider { name: "OpenAI".to_string(), + display_name: "OpenAI".to_string(), format: InferenceProviderFormat::OpenAiResponses, api_base_url: "https://api.openai.com/v1".to_string(), api_key: "secret".to_string(), @@ -830,6 +862,7 @@ mod tests { let error = store .create(CreateInferenceProvider { name: "OpenAI".to_string(), + display_name: "OpenAI".to_string(), format: InferenceProviderFormat::OpenAiResponses, api_base_url: "https://api.openai.com/v1".to_string(), api_key: "secret".to_string(), @@ -853,6 +886,7 @@ mod tests { &provider.id, UpdateInferenceProvider { name: None, + display_name: None, format: None, api_base_url: None, api_key: None, @@ -869,6 +903,7 @@ mod tests { &provider.id, UpdateInferenceProvider { name: None, + display_name: None, format: None, api_base_url: None, api_key: None, @@ -887,6 +922,7 @@ mod tests { let provider = store .create(CreateInferenceProvider { name: "OpenAI".to_string(), + display_name: "OpenAI".to_string(), format: InferenceProviderFormat::OpenAiResponses, api_base_url: "https://api.openai.com/v1".to_string(), api_key: "secret".to_string(), @@ -914,6 +950,7 @@ mod tests { let error = store .create(CreateInferenceProvider { name: "OpenAI".to_string(), + display_name: "OpenAI".to_string(), format: InferenceProviderFormat::OpenAiResponses, api_base_url: "https://api.openai.com/v1".to_string(), api_key: "secret".to_string(), From caacd4863cc13a7dbe6f1f11696b23a3d45ad8c5 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sun, 26 Apr 2026 15:03:47 +0800 Subject: [PATCH 20/62] fix(desktop): edit matched opencode providers in store --- .../desktop/src/pages/inference-providers.tsx | 10 ++++++- .../inference-providers/opencode-panel.tsx | 27 +++++++++++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index e97cb7fa..8c4f5f62 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -992,6 +992,12 @@ export default function InferenceProvidersPage() { setPanel({ type: "detail" }); }; + const handleEditProviderByName = (name: string) => { + const provider = providers.find((provider) => provider.name === name); + setSelectedName(name); + setPanel(provider ? { type: "edit", provider } : { type: "detail" }); + }; + if (isLoading) { return (
@@ -1152,7 +1158,9 @@ export default function InferenceProvidersPage() {
{panel.type === "agent" && panel.agentId === "opencode" && ( - + )} {panel.type === "agent" && panel.agentId === "codex" && ( diff --git a/crates/desktop/src/pages/inference-providers/opencode-panel.tsx b/crates/desktop/src/pages/inference-providers/opencode-panel.tsx index 06251e1e..a64a6475 100644 --- a/crates/desktop/src/pages/inference-providers/opencode-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/opencode-panel.tsx @@ -439,7 +439,11 @@ function ProviderRow({ isIconOnly variant="ghost" size="sm" - aria-label={t("editOpenCodeProvider")} + aria-label={t( + matchedProvider + ? "editInferenceProvider" + : "editOpenCodeProvider", + )} onPress={onEdit} > @@ -467,7 +471,11 @@ function ProviderRow({ ); } -export function OpenCodeInferenceProviderPanel() { +export function OpenCodeInferenceProviderPanel({ + onEditInferenceProvider, +}: { + onEditInferenceProvider: (providerName: string) => void; +}) { const { t } = useTranslation(); const api = useApi(); const queryClient = useQueryClient(); @@ -525,6 +533,16 @@ export function OpenCodeInferenceProviderPanel() { }, }); + const handleEditProvider = (provider: AgentProviderResponse) => { + const matchedProvider = provider.matched_inference_provider; + if (matchedProvider) { + onEditInferenceProvider(matchedProvider.name); + return; + } + + setProviderDialog({ type: "edit", provider }); + }; + return ( <>
@@ -621,10 +639,9 @@ export function OpenCodeInferenceProviderPanel() { provider.id } onEdit={() => - setProviderDialog({ - type: "edit", + handleEditProvider( provider, - }) + ) } onSync={() => syncMutation.mutate( From b259fc927108c2cc0d252af1f3db1176b4c840b5 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sun, 26 Apr 2026 16:01:22 +0800 Subject: [PATCH 21/62] feat(inference): support codex providers Add Codex config.toml provider CRUD using Responses API providers only, wire it through API routes, and expose Codex provider management in the desktop inference page. --- Cargo.lock | 36 +- Cargo.toml | 1 + crates/api/src/lib.rs | 5 + crates/api/src/routes/inference.rs | 135 +++- crates/desktop/src/lib/api.ts | 35 + crates/desktop/src/lib/locales/en.ts | 20 + crates/desktop/src/lib/locales/zh-Hans.ts | 19 + crates/desktop/src/lib/locales/zh-Hant.ts | 19 + .../desktop/src/pages/inference-providers.tsx | 4 +- .../pages/inference-providers/codex-panel.tsx | 741 +++++++++++++++++- .../src/requests/inference-providers.ts | 77 ++ crates/inference/Cargo.toml | 1 + crates/inference/src/agent.rs | 10 +- crates/inference/src/codex/files.rs | 57 ++ crates/inference/src/codex/mapping.rs | 192 +++++ crates/inference/src/codex/mod.rs | 297 +++++++ crates/inference/src/codex/tests.rs | 198 +++++ crates/inference/src/lib.rs | 2 + 18 files changed, 1809 insertions(+), 40 deletions(-) create mode 100644 crates/inference/src/codex/files.rs create mode 100644 crates/inference/src/codex/mapping.rs create mode 100644 crates/inference/src/codex/mod.rs create mode 100644 crates/inference/src/codex/tests.rs diff --git a/Cargo.lock b/Cargo.lock index bdb3db12..165fa895 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,6 +138,7 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "tokio", + "toml_edit 0.22.27", "uuid", ] @@ -3038,7 +3039,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ "heck 0.4.1", - "proc-macro-crate 2.0.2", + "proc-macro-crate 2.0.0", "proc-macro-error", "proc-macro2", "quote", @@ -5189,11 +5190,10 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime 0.6.3", "toml_edit 0.20.2", ] @@ -7793,7 +7793,7 @@ checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", "serde_spanned 0.6.9", - "toml_datetime 0.6.3", + "toml_datetime 0.6.11", "toml_edit 0.20.2", ] @@ -7829,9 +7829,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] @@ -7861,7 +7861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.13.0", - "toml_datetime 0.6.3", + "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -7874,10 +7874,22 @@ dependencies = [ "indexmap 2.13.0", "serde", "serde_spanned 0.6.9", - "toml_datetime 0.6.3", + "toml_datetime 0.6.11", "winnow 0.5.40", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + [[package]] name = "toml_edit" version = "0.25.5+spec-1.1.0" @@ -7899,6 +7911,12 @@ dependencies = [ "winnow 1.0.0", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "toml_writer" version = "1.1.1+spec-1.1.0" diff --git a/Cargo.toml b/Cargo.toml index bf6ec85d..7d187726 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ serde_yaml = "0.9" # TOML parsing toml = "1.0" +toml_edit = "0.22" # Interactive CLI inquire = "0.9" diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 4f6ecc57..9607819b 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -168,10 +168,15 @@ pub async fn start(options: ApiOptions) -> Result<(), rocket::Error> { routes::credentials::delete_credential, routes::inference::list_inference_providers, routes::inference::list_opencode_providers, + routes::inference::list_codex_providers, routes::inference::create_opencode_provider, + routes::inference::create_codex_provider, routes::inference::update_opencode_provider, + routes::inference::update_codex_provider, routes::inference::sync_opencode_provider, + routes::inference::sync_codex_provider, routes::inference::delete_opencode_provider, + routes::inference::delete_codex_provider, routes::inference::get_inference_provider_password, routes::inference::create_inference_provider, routes::inference::update_inference_provider, diff --git a/crates/api/src/routes/inference.rs b/crates/api/src/routes/inference.rs index 34b2f594..06e18d20 100644 --- a/crates/api/src/routes/inference.rs +++ b/crates/api/src/routes/inference.rs @@ -1,6 +1,6 @@ use aghub_inference::{ - AgentProviderAdapter, AgentProviderBinding, InferenceProvider, - InferenceProviderRepository, InferenceProviderStore, + AgentProviderAdapter, AgentProviderBinding, CodexProviderAdapter, + InferenceProvider, InferenceProviderRepository, InferenceProviderStore, OpenCodeProviderAdapter, }; use rocket::http::Status; @@ -43,6 +43,10 @@ fn opencode_adapter() -> Result { OpenCodeProviderAdapter::global().map_err(ApiError::from) } +fn codex_adapter() -> Result { + CodexProviderAdapter::global().map_err(ApiError::from) +} + fn get_inventory_provider( store: &InferenceProviderStore, id: &str, @@ -85,15 +89,13 @@ fn inventory_providers_with_api_keys( fn find_matching_inventory_provider( inventory: &[(InferenceProvider, String)], - adapter: &OpenCodeProviderAdapter, binding: &AgentProviderBinding, + agent_api_key: Option, ) -> Result, ApiError> { let Some(api_base_url) = binding.api_base_url.as_deref() else { return Ok(None); }; - let Some(agent_api_key) = - adapter.api_key(&binding.id).map_err(ApiError::from)? - else { + let Some(agent_api_key) = agent_api_key else { return Ok(None); }; @@ -114,8 +116,26 @@ fn opencode_provider_response( adapter: &OpenCodeProviderAdapter, binding: AgentProviderBinding, ) -> Result { + let agent_api_key = adapter.api_key(&binding.id).map_err(ApiError::from)?; + let matched = + find_matching_inventory_provider(inventory, &binding, agent_api_key)?; + let response = AgentProviderResponse::from(binding); + Ok(match matched { + Some((provider, _)) => { + response.with_matched_inference_provider(&provider) + } + None => response, + }) +} + +fn codex_provider_response( + inventory: &[(InferenceProvider, String)], + adapter: &CodexProviderAdapter, + binding: AgentProviderBinding, +) -> Result { + let agent_api_key = adapter.api_key(&binding.id).map_err(ApiError::from)?; let matched = - find_matching_inventory_provider(inventory, adapter, &binding)?; + find_matching_inventory_provider(inventory, &binding, agent_api_key)?; let response = AgentProviderResponse::from(binding); Ok(match matched { Some((provider, _)) => { @@ -157,6 +177,23 @@ pub fn list_opencode_providers( Ok(Json(providers)) } +#[get("/inference/agents/codex/providers")] +pub fn list_codex_providers( + state: &State, +) -> ApiResult> { + let store = store(state); + let adapter = codex_adapter()?; + let inventory = inventory_providers_with_api_keys(&store)?; + let providers = adapter + .load_providers() + .map_err(ApiError::from)? + .providers + .into_iter() + .map(|binding| codex_provider_response(&inventory, &adapter, binding)) + .collect::, _>>()?; + Ok(Json(providers)) +} + #[post("/inference/agents/opencode/providers", data = "")] pub fn create_opencode_provider( state: &State, @@ -172,6 +209,21 @@ pub fn create_opencode_provider( Ok((Status::Created, Json(binding.into()))) } +#[post("/inference/agents/codex/providers", data = "")] +pub fn create_codex_provider( + state: &State, + body: Json, +) -> ApiCreated { + let store = store(state); + let (provider, api_key) = + get_inventory_provider(&store, &body.inference_provider_id)?; + let binding = codex_adapter()? + .add_inventory_provider(&provider, &api_key) + .map_err(ApiError::from)?; + + Ok((Status::Created, Json(binding.into()))) +} + #[put("/inference/agents/opencode/providers/", data = "")] pub fn update_opencode_provider( id: &str, @@ -185,6 +237,19 @@ pub fn update_opencode_provider( Ok(Json(binding.into())) } +#[put("/inference/agents/codex/providers/", data = "")] +pub fn update_codex_provider( + id: &str, + body: Json, +) -> ApiResult { + let body = body.into_inner(); + let binding = codex_adapter()? + .update_provider(id, body.name.as_deref(), body.api_key.as_deref()) + .map_err(ApiError::from)?; + + Ok(Json(binding.into())) +} + #[post("/inference/agents/opencode/providers//sync")] pub fn sync_opencode_provider( state: &State, @@ -206,8 +271,9 @@ pub fn sync_opencode_provider( "RESOURCE_NOT_FOUND", ) })?; + let agent_api_key = adapter.api_key(&binding.id).map_err(ApiError::from)?; let Some((provider, api_key)) = - find_matching_inventory_provider(&inventory, &adapter, &binding)? + find_matching_inventory_provider(&inventory, &binding, agent_api_key)? else { return Err(ApiError::new( Status::UnprocessableEntity, @@ -229,6 +295,51 @@ pub fn sync_opencode_provider( )) } +#[post("/inference/agents/codex/providers//sync")] +pub fn sync_codex_provider( + state: &State, + id: &str, +) -> ApiResult { + let store = store(state); + let adapter = codex_adapter()?; + let inventory = inventory_providers_with_api_keys(&store)?; + let binding = adapter + .load_providers() + .map_err(ApiError::from)? + .providers + .into_iter() + .find(|provider| provider.id == id) + .ok_or_else(|| { + ApiError::new( + Status::NotFound, + format!("Codex provider '{id}' not found"), + "RESOURCE_NOT_FOUND", + ) + })?; + let agent_api_key = adapter.api_key(&binding.id).map_err(ApiError::from)?; + let Some((provider, api_key)) = + find_matching_inventory_provider(&inventory, &binding, agent_api_key)? + else { + return Err(ApiError::new( + Status::UnprocessableEntity, + format!( + "Codex provider '{id}' is not backed by an aghub inference \ + provider" + ), + "UNRECOGNIZED_PROVIDER", + )); + }; + + let updated = adapter + .add_provider(id, &provider, &api_key) + .map_err(ApiError::from)?; + + Ok(Json( + AgentProviderResponse::from(updated) + .with_matched_inference_provider(&provider), + )) +} + #[delete("/inference/agents/opencode/providers/")] pub fn delete_opencode_provider(id: &str) -> ApiNoContent { opencode_adapter()? @@ -237,6 +348,14 @@ pub fn delete_opencode_provider(id: &str) -> ApiNoContent { Ok(NoContent) } +#[delete("/inference/agents/codex/providers/")] +pub fn delete_codex_provider(id: &str) -> ApiNoContent { + codex_adapter()? + .remove_provider(id) + .map_err(ApiError::from)?; + Ok(NoContent) +} + #[get("/inference/providers//password")] pub fn get_inference_provider_password( state: &State, diff --git a/crates/desktop/src/lib/api.ts b/crates/desktop/src/lib/api.ts index 54379ef3..239c94cd 100644 --- a/crates/desktop/src/lib/api.ts +++ b/crates/desktop/src/lib/api.ts @@ -489,6 +489,9 @@ export function createApi(baseUrl: string) { listOpenCode(): Promise { return client.get("inference/agents/opencode/providers").json(); }, + listCodex(): Promise { + return client.get("inference/agents/codex/providers").json(); + }, getPassword( name: string, ): Promise { @@ -512,6 +515,13 @@ export function createApi(baseUrl: string) { .post("inference/agents/opencode/providers", { json: body }) .json(); }, + createCodex( + body: CreateAgentProviderRequest, + ): Promise { + return client + .post("inference/agents/codex/providers", { json: body }) + .json(); + }, update( name: string, body: UpdateInferenceProviderRequest, @@ -533,6 +543,17 @@ export function createApi(baseUrl: string) { ) .json(); }, + updateCodex( + id: string, + body: UpdateAgentProviderRequest, + ): Promise { + return client + .put( + `inference/agents/codex/providers/${encodeURIComponent(id)}`, + { json: body }, + ) + .json(); + }, syncOpenCode(id: string): Promise { return client .post( @@ -540,6 +561,13 @@ export function createApi(baseUrl: string) { ) .json(); }, + syncCodex(id: string): Promise { + return client + .post( + `inference/agents/codex/providers/${encodeURIComponent(id)}/sync`, + ) + .json(); + }, async createModel( providerName: string, modelName: string, @@ -591,6 +619,13 @@ export function createApi(baseUrl: string) { ) .then(() => undefined); }, + deleteCodex(id: string): Promise { + return client + .delete( + `inference/agents/codex/providers/${encodeURIComponent(id)}`, + ) + .then(() => undefined); + }, }, }; } diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index 9b709bab..8b004c41 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -117,6 +117,26 @@ export default { syncOpenCodeProvider: "Update OpenCode provider", syncOpenCodeProviderFromInferenceProvider: "Update models and config from {{name}}", + createCodexProvider: "Add Codex Provider", + editCodexProvider: "Edit Codex Provider", + deleteCodexProvider: "Delete Codex Provider", + deleteCodexProviderConfirm: 'Delete "{{name}}" from Codex config.toml?', + refreshCodexProviders: "Refresh Codex providers", + noCodexProviders: "No Codex providers configured.", + noInferenceProvidersForCodex: + "Create an OpenAI Responses inference provider first, then select it here.", + codexProviderCreated: "Codex provider created", + codexProviderUpdated: "Codex provider updated", + codexProviderDeleted: "Codex provider deleted", + codexProviderDeleteError: "Failed to delete Codex provider", + codexProviderSynced: "Codex provider updated", + codexProviderSyncError: "Failed to update Codex provider", + codexBuiltInProvider: "Built-in provider", + codexBuiltInProviderInfo: + "No Aghub OpenAI Responses provider has the same API Base URL and API key. This was likely added directly in Codex.", + syncCodexProvider: "Update Codex provider", + syncCodexProviderFromInferenceProvider: + "Update provider config from {{name}}", agentProviderSourceCustom: "Custom", agentProviderSourceBuiltIn: "Built-in", agentProviderSourceClosedSlot: "Closed slot", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index 45b844c0..4bd9e42e 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -112,6 +112,25 @@ export default { "未在 Aghub 推理 Provider 中找到相同 API Base URL 和 API key,疑似用户直接在 OpenCode 里添加。", syncOpenCodeProvider: "更新 OpenCode Provider", syncOpenCodeProviderFromInferenceProvider: "从 {{name}} 更新模型列表和配置", + createCodexProvider: "添加 Codex Provider", + editCodexProvider: "编辑 Codex Provider", + deleteCodexProvider: "删除 Codex Provider", + deleteCodexProviderConfirm: '确定从 Codex config.toml 删除"{{name}}"吗?', + refreshCodexProviders: "刷新 Codex Provider", + noCodexProviders: "暂无 Codex Provider。", + noInferenceProvidersForCodex: + "请先创建一个 OpenAI Responses 推理 Provider,然后在这里选择。", + codexProviderCreated: "Codex Provider 已创建", + codexProviderUpdated: "Codex Provider 已更新", + codexProviderDeleted: "Codex Provider 已删除", + codexProviderDeleteError: "删除 Codex Provider 失败", + codexProviderSynced: "Codex Provider 已更新", + codexProviderSyncError: "更新 Codex Provider 失败", + codexBuiltInProvider: "内置 provider", + codexBuiltInProviderInfo: + "未在 Aghub OpenAI Responses Provider 中找到相同 API Base URL 和 API key,疑似用户直接在 Codex 里添加。", + syncCodexProvider: "更新 Codex Provider", + syncCodexProviderFromInferenceProvider: "从 {{name}} 更新 Provider 配置", agentProviderSourceCustom: "自定义", agentProviderSourceBuiltIn: "内置", agentProviderSourceClosedSlot: "封闭槽位", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index bba27db2..1984c033 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -112,6 +112,25 @@ export default { "未在 Aghub 推理 Provider 中找到相同 API Base URL 和 API key,疑似使用者直接在 OpenCode 裡新增。", syncOpenCodeProvider: "更新 OpenCode Provider", syncOpenCodeProviderFromInferenceProvider: "從 {{name}} 更新模型列表和配置", + createCodexProvider: "新增 Codex Provider", + editCodexProvider: "編輯 Codex Provider", + deleteCodexProvider: "刪除 Codex Provider", + deleteCodexProviderConfirm: "確定從 Codex config.toml 刪除「{{name}}」嗎?", + refreshCodexProviders: "重新整理 Codex Provider", + noCodexProviders: "尚無 Codex Provider。", + noInferenceProvidersForCodex: + "請先建立一個 OpenAI Responses 推理 Provider,然後在這裡選擇。", + codexProviderCreated: "Codex Provider 已建立", + codexProviderUpdated: "Codex Provider 已更新", + codexProviderDeleted: "Codex Provider 已刪除", + codexProviderDeleteError: "刪除 Codex Provider 失敗", + codexProviderSynced: "Codex Provider 已更新", + codexProviderSyncError: "更新 Codex Provider 失敗", + codexBuiltInProvider: "內建 provider", + codexBuiltInProviderInfo: + "未在 Aghub OpenAI Responses Provider 中找到相同 API Base URL 和 API key,疑似使用者直接在 Codex 裡新增。", + syncCodexProvider: "更新 Codex Provider", + syncCodexProviderFromInferenceProvider: "從 {{name}} 更新 Provider 配置", agentProviderSourceCustom: "自訂", agentProviderSourceBuiltIn: "內建", agentProviderSourceClosedSlot: "封閉槽位", diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index 8c4f5f62..ca36fa8f 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -1164,7 +1164,9 @@ export default function InferenceProvidersPage() { )} {panel.type === "agent" && panel.agentId === "codex" && ( - + )} {panel.type === "create" && ( diff --git a/crates/desktop/src/pages/inference-providers/codex-panel.tsx b/crates/desktop/src/pages/inference-providers/codex-panel.tsx index d145a0c2..70c508c5 100644 --- a/crates/desktop/src/pages/inference-providers/codex-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/codex-panel.tsx @@ -1,29 +1,728 @@ -import { Card } from "@heroui/react"; +import { + ArrowPathIcon, + PencilIcon, + PlusIcon, + QuestionMarkCircleIcon, + ServerIcon, + TrashIcon, +} from "@heroicons/react/24/solid"; +import { + Alert, + AlertDialog, + Button, + Card, + FieldError, + Input, + Label, + ListBox, + Modal, + Select, + Spinner, + TextField, + Tooltip, + toast, +} from "@heroui/react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useEffect, useMemo, useState } from "react"; +import type { FormEvent } from "react"; +import { useTranslation } from "react-i18next"; +import type { + AgentProviderResponse, + InferenceProviderResponse, +} from "../../generated/dto"; +import { useApi } from "../../hooks/use-api"; import { AgentIcon } from "../../lib/agent-icons"; +import { cn } from "../../lib/utils"; +import { + codexProviderListQueryOptions, + createCodexProviderMutationOptions, + deleteCodexProviderMutationOptions, + inferenceProviderListQueryOptions, + syncCodexProviderMutationOptions, + updateCodexProviderMutationOptions, +} from "../../requests/inference-providers"; + +type ProviderDialogMode = + | { type: "create" } + | { type: "edit"; provider: AgentProviderResponse }; + +function CodexCreateProviderDialog({ + isOpen, + inventoryProviders, + isInventoryLoading, + onClose, +}: { + isOpen: boolean; + inventoryProviders: InferenceProviderResponse[]; + isInventoryLoading: boolean; + onClose: () => void; +}) { + const { t } = useTranslation(); + const api = useApi(); + const queryClient = useQueryClient(); + const [selectedProviderId, setSelectedProviderId] = useState(""); + + const responseProviders = useMemo( + () => + inventoryProviders.filter( + (provider) => provider.format === "openai_responses", + ), + [inventoryProviders], + ); + const defaultProviderId = responseProviders[0]?.id ?? ""; + useEffect(() => { + if (!isOpen) return; + setSelectedProviderId((current) => current || defaultProviderId); + }, [defaultProviderId, isOpen]); + + const createMutation = useMutation({ + ...createCodexProviderMutationOptions({ + api, + queryClient, + onSuccess: async () => { + toast.success(t("codexProviderCreated")); + onClose(); + }, + }), + }); + + const activeError = createMutation.error; + const isPending = createMutation.isPending; + const hasResponseProviders = responseProviders.length > 0; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + if (!selectedProviderId) return; + + createMutation.mutate({ + inference_provider_id: selectedProviderId, + }); + }; -export function CodexInferenceProviderPanel() { return ( -
-
- - -
- { + if (!open) onClose(); + }} + > + + + + + + {t("createCodexProvider")} + + +
+ + {activeError && ( + + + + + {activeError instanceof Error + ? activeError.message + : String(activeError)} + + + + )} + + {isInventoryLoading && ( +
+ +
+ )} + + {!isInventoryLoading && !hasResponseProviders && ( + + + + + {t("noInferenceProvidersForCodex")} + + + + )} + + {!isInventoryLoading && hasResponseProviders && ( + + )} +
+ + + + +
+
+
+ + ); +} + +function CodexEditProviderDialog({ + isOpen, + provider, + onClose, +}: { + isOpen: boolean; + provider: AgentProviderResponse; + onClose: () => void; +}) { + const { t } = useTranslation(); + const api = useApi(); + const queryClient = useQueryClient(); + const [name, setName] = useState(provider.name); + const [apiKey, setApiKey] = useState(""); + const [nameError, setNameError] = useState(null); + + useEffect(() => { + if (!isOpen) return; + setName(provider.name); + setApiKey(""); + setNameError(null); + }, [isOpen, provider.id, provider.name]); + + const updateMutation = useMutation({ + ...updateCodexProviderMutationOptions({ + api, + queryClient, + onSuccess: async () => { + toast.success(t("codexProviderUpdated")); + onClose(); + }, + }), + }); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + const trimmedName = name.trim(); + if (!trimmedName) { + setNameError(t("validationProviderNameRequired")); + return; + } + + const trimmedApiKey = apiKey.trim(); + updateMutation.mutate({ + id: provider.id, + body: { + name: trimmedName === provider.name ? null : trimmedName, + api_key: trimmedApiKey ? trimmedApiKey : null, + }, + }); + }; + + return ( + { + if (!open) onClose(); + }} + > + + + + + {t("editCodexProvider")} + +
+ + {updateMutation.error && ( + + + + + {updateMutation.error instanceof + Error + ? updateMutation.error.message + : String(updateMutation.error)} + + + + )} + + + + { + setName(event.target.value); + if (nameError) setNameError(null); + }} + placeholder={t("providerNamePlaceholder")} + variant="secondary" + /> + {nameError && ( + {nameError} + )} + + + + + + setApiKey(event.target.value) + } + placeholder={t( + "providerApiKeyEditPlaceholder", + )} + variant="secondary" + /> + + + + + + +
+
+
+
+ ); +} + +function ProviderRow({ + provider, + isSyncing, + onEdit, + onSync, + onDelete, +}: { + provider: AgentProviderResponse; + isSyncing: boolean; + onEdit: () => void; + onSync: () => void; + onDelete: () => void; +}) { + const { t } = useTranslation(); + const matchedProvider = provider.matched_inference_provider; + + return ( +
+
+
+ + + {provider.id !== provider.name && ( + + {provider.id} + + )} +
+
+ {matchedProvider ? ( + + {t("providerModels")}: {matchedProvider.model_count} + + ) : ( + + {t("codexBuiltInProvider")} + + + + + + + + {t("codexBuiltInProviderInfo")} + + + + )} +
+
+ +
+ {matchedProvider && ( + + +
- - - + size="sm" + aria-label={t("syncCodexProvider")} + isPending={isSyncing} + onPress={onSync} + > + + + + + {t("syncCodexProviderFromInferenceProvider", { + name: matchedProvider.display_name, + })} + + + )} + + + + + {t("edit")} + + + + + + {t("delete")} +
); } + +export function CodexInferenceProviderPanel({ + onEditInferenceProvider, +}: { + onEditInferenceProvider: (providerName: string) => void; +}) { + const { t } = useTranslation(); + const api = useApi(); + const queryClient = useQueryClient(); + const [providerDialog, setProviderDialog] = + useState(null); + const [deleteTarget, setDeleteTarget] = + useState(null); + + const { + data: providers = [], + isLoading, + isFetching, + refetch, + } = useQuery({ + ...codexProviderListQueryOptions({ api }), + }); + const { data: inventoryProviders = [], isLoading: isInventoryLoading } = + useQuery({ + ...inferenceProviderListQueryOptions({ api }), + }); + + const deleteMutation = useMutation({ + ...deleteCodexProviderMutationOptions({ + api, + queryClient, + onSuccess: async () => { + setDeleteTarget(null); + toast.success(t("codexProviderDeleted")); + }, + }), + onError: (error) => { + console.error("Failed to delete Codex provider:", error); + toast.danger( + error instanceof Error + ? error.message + : t("codexProviderDeleteError"), + ); + }, + }); + const syncMutation = useMutation({ + ...syncCodexProviderMutationOptions({ + api, + queryClient, + onSuccess: async () => { + toast.success(t("codexProviderSynced")); + }, + }), + onError: (error) => { + console.error("Failed to sync Codex provider:", error); + toast.danger( + error instanceof Error + ? error.message + : t("codexProviderSyncError"), + ); + }, + }); + + const handleEditProvider = (provider: AgentProviderResponse) => { + const matchedProvider = provider.matched_inference_provider; + if (matchedProvider) { + onEditInferenceProvider(matchedProvider.name); + return; + } + + setProviderDialog({ type: "edit", provider }); + }; + + return ( + <> +
+
+ + +
+ +
+

+ Codex +

+
+
+
+ + + + + + {t("refresh")} + + + +
+
+ + + {isLoading ? ( +
+ +
+ ) : ( + <> + {providers.length === 0 ? ( +
+

+ {t("noCodexProviders")} +

+ +
+ ) : ( +
+ {providers.map((provider) => ( + + handleEditProvider( + provider, + ) + } + onSync={() => + syncMutation.mutate( + provider.id, + ) + } + onDelete={() => + setDeleteTarget( + provider, + ) + } + /> + ))} +
+ )} + + )} +
+
+
+
+ + setProviderDialog(null)} + /> + {providerDialog?.type === "edit" && ( + setProviderDialog(null)} + /> + )} + + { + if (!open) setDeleteTarget(null); + }} + > + + + + + + + {t("deleteCodexProvider")} + + + + {t("deleteCodexProviderConfirm", { + name: deleteTarget?.name, + })} + + + + + + + + + + ); +} diff --git a/crates/desktop/src/requests/inference-providers.ts b/crates/desktop/src/requests/inference-providers.ts index b433cc7a..8f81bb16 100644 --- a/crates/desktop/src/requests/inference-providers.ts +++ b/crates/desktop/src/requests/inference-providers.ts @@ -46,6 +46,19 @@ export function openCodeProviderListQueryOptions({ }); } +export function codexProviderListQueryOptions({ + api, + enabled = true, + staleTime = 30_000, +}: InferenceProviderListQueryParams) { + return queryOptions({ + queryKey: queryKeys.inferenceProviders.agent("codex"), + queryFn: () => api.inferenceProviders.listCodex(), + enabled, + staleTime, + }); +} + export async function invalidateInferenceProviderQueries( queryClient: QueryClient, ) { @@ -62,6 +75,12 @@ export async function invalidateOpenCodeProviderQueries( }); } +export async function invalidateCodexProviderQueries(queryClient: QueryClient) { + await queryClient.invalidateQueries({ + queryKey: queryKeys.inferenceProviders.agent("codex"), + }); +} + interface CreateInferenceProviderMutationParams { api: ApiClient; queryClient: QueryClient; @@ -104,6 +123,21 @@ export function createOpenCodeProviderMutationOptions({ }); } +export function createCodexProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: CreateAgentProviderMutationParams) { + return mutationOptions({ + mutationFn: (body: CreateAgentProviderRequest) => + api.inferenceProviders.createCodex(body), + onSuccess: async (data) => { + await invalidateCodexProviderQueries(queryClient); + await onSuccess?.(data); + }, + }); +} + interface CreateInferenceModelVariables { providerName: string; modelName: string; @@ -194,6 +228,21 @@ export function updateOpenCodeProviderMutationOptions({ }); } +export function updateCodexProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: UpdateAgentProviderMutationParams) { + return mutationOptions({ + mutationFn: ({ id, body }: UpdateAgentProviderVariables) => + api.inferenceProviders.updateCodex(id, body), + onSuccess: async (data, variables) => { + await invalidateCodexProviderQueries(queryClient); + await onSuccess?.(data, variables); + }, + }); +} + interface SyncAgentProviderMutationParams { api: ApiClient; queryClient: QueryClient; @@ -217,6 +266,20 @@ export function syncOpenCodeProviderMutationOptions({ }); } +export function syncCodexProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: SyncAgentProviderMutationParams) { + return mutationOptions({ + mutationFn: (id: string) => api.inferenceProviders.syncCodex(id), + onSuccess: async (data, id) => { + await invalidateCodexProviderQueries(queryClient); + await onSuccess?.(data, id); + }, + }); +} + interface UpdateInferenceModelVariables { providerName: string; modelName: string; @@ -295,6 +358,20 @@ export function deleteOpenCodeProviderMutationOptions({ }); } +export function deleteCodexProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: DeleteAgentProviderMutationParams) { + return mutationOptions({ + mutationFn: (id: string) => api.inferenceProviders.deleteCodex(id), + onSuccess: async () => { + await invalidateCodexProviderQueries(queryClient); + await onSuccess?.(); + }, + }); +} + interface DeleteInferenceModelVariables { providerName: string; modelName: string; diff --git a/crates/inference/Cargo.toml b/crates/inference/Cargo.toml index dfcc3e49..5a8305bc 100644 --- a/crates/inference/Cargo.toml +++ b/crates/inference/Cargo.toml @@ -10,6 +10,7 @@ description = "Inference provider configuration storage for aghub" aghub-json = { path = "../json" } serde = { workspace = true } serde_json = { workspace = true } +toml_edit = { workspace = true } thiserror = { workspace = true } dirs = { workspace = true } keyring = { version = "3", features = [ diff --git a/crates/inference/src/agent.rs b/crates/inference/src/agent.rs index 79252b02..dd7379b5 100644 --- a/crates/inference/src/agent.rs +++ b/crates/inference/src/agent.rs @@ -141,6 +141,13 @@ impl AgentCredentialSupport { agent_credential_store: false, }; + /// Environment variables, inline config values, or agent auth. + pub const ENV_VAR_INLINE_OR_AGENT_STORE: Self = Self { + env_var_reference: true, + inline_api_key: true, + agent_credential_store: true, + }; + /// Environment variables plus the agent's own credential store. pub const ENV_VAR_OR_AGENT_STORE: Self = Self { env_var_reference: true, @@ -668,12 +675,13 @@ mod tests { fn codex_caps_support_default_provider_and_registry() { let caps = AgentProviderCapabilities::registry( AgentProviderDefaultSupport::PROVIDER_AND_MODEL, - AgentCredentialSupport::ENV_VAR, + AgentCredentialSupport::ENV_VAR_INLINE_OR_AGENT_STORE, BuiltInProviderSupport::IMMUTABLE, ); assert!(caps.supports(AgentProviderCapability::DefaultProvider)); assert!(caps.supports(AgentProviderCapability::CustomProviders)); + assert!(caps.supports(AgentProviderCapability::InlineApiKeyCredentials)); assert!( !caps.supports(AgentProviderCapability::OverrideBuiltInProviders) ); diff --git a/crates/inference/src/codex/files.rs b/crates/inference/src/codex/files.rs new file mode 100644 index 00000000..aa1dc6e2 --- /dev/null +++ b/crates/inference/src/codex/files.rs @@ -0,0 +1,57 @@ +//! File I/O for Codex provider config. + +use std::fs; +use std::path::{Path, PathBuf}; + +use toml_edit::DocumentMut; + +use super::AGENT_ID; +use crate::error::{InferenceProviderError, Result}; + +pub(super) fn read_config(path: &Path) -> Result { + let content = match fs::read_to_string(path) { + Ok(content) => content, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return Ok(DocumentMut::new()); + } + Err(error) => return Err(error.into()), + }; + + content + .parse::() + .map_err(|error| invalid_config(path, error.to_string())) +} + +pub(super) fn write_config(path: &Path, config: &DocumentMut) -> Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, config.to_string())?; + Ok(()) +} + +pub(super) fn default_global_config_path() -> Result { + let codex_home = std::env::var_os("CODEX_HOME") + .map(PathBuf::from) + .or_else(|| dirs::home_dir().map(|home| home.join(".codex"))) + .ok_or_else(home_dir_error)?; + Ok(codex_home.join("config.toml")) +} + +fn home_dir_error() -> InferenceProviderError { + InferenceProviderError::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + "home directory not found", + )) +} + +pub(super) fn invalid_config( + path: &Path, + message: impl Into, +) -> InferenceProviderError { + InferenceProviderError::InvalidAgentProviderConfig { + agent_id: AGENT_ID.to_string(), + path: path.display().to_string(), + message: message.into(), + } +} diff --git a/crates/inference/src/codex/mapping.rs b/crates/inference/src/codex/mapping.rs new file mode 100644 index 00000000..646120f8 --- /dev/null +++ b/crates/inference/src/codex/mapping.rs @@ -0,0 +1,192 @@ +//! Mapping between Codex config.toml and normalized agent state. + +use toml_edit::{value, Item, Table}; + +use crate::agent::{ + AgentProviderBinding, AgentProviderCredential, AgentProviderModel, + AgentProviderSource, +}; +use crate::error::{InferenceProviderError, Result}; +use crate::model::InferenceProviderFormat; + +const WIRE_API_RESPONSES: &str = "responses"; +const AUTHORIZATION_HEADER: &str = "Authorization"; +const RESERVED_PROVIDER_IDS: &[&str] = &["openai", "ollama", "lmstudio"]; + +pub(super) fn binding_from_table( + provider_id: &str, + table: &Table, +) -> Result { + Ok(AgentProviderBinding { + id: clean_provider_id(provider_id)?, + source_provider_id: None, + name: string_field(table, "name") + .unwrap_or_else(|| provider_id.to_string()), + format: format_from_table(table), + api_base_url: string_field(table, "base_url"), + credential: credential_from_table(table), + models: Vec::::new(), + source: AgentProviderSource::Custom, + }) +} + +pub(super) fn provider_table_from_binding( + binding: &AgentProviderBinding, + api_key: Option<&str>, + existing: Option<&Table>, +) -> Table { + let mut table = existing.cloned().unwrap_or_default(); + table["name"] = value(binding.name.clone()); + if let Some(api_base_url) = &binding.api_base_url { + table["base_url"] = value(api_base_url.clone()); + } + table["wire_api"] = value(WIRE_API_RESPONSES); + table.remove("env_key"); + table.remove("requires_openai_auth"); + table.remove("auth"); + if let Some(api_key) = api_key { + table["experimental_bearer_token"] = value(api_key.to_string()); + } + table +} + +pub(super) fn api_key_from_table(table: &Table) -> Option { + if let Some(token) = string_field(table, "experimental_bearer_token") { + return Some(token); + } + + if let Some(env_key) = string_field(table, "env_key") { + return std::env::var(env_key).ok(); + } + + let auth = table + .get("http_headers") + .and_then(Item::as_table_like) + .and_then(|headers| headers.get(AUTHORIZATION_HEADER)) + .and_then(Item::as_str)?; + auth.strip_prefix("Bearer ") + .map(str::trim) + .filter(|token| !token.is_empty()) + .map(ToString::to_string) +} + +pub(super) fn provider_id_from_name(name: &str) -> String { + let mut out = String::new(); + let mut previous_was_dash = false; + + for ch in name.chars().flat_map(char::to_lowercase) { + if ch.is_ascii_alphanumeric() { + out.push(ch); + previous_was_dash = false; + } else if !previous_was_dash && !out.is_empty() { + out.push('-'); + previous_was_dash = true; + } + } + + while out.ends_with('-') { + out.pop(); + } + + if out.is_empty() { + "provider".to_string() + } else if is_reserved_provider_id(&out) { + format!("aghub-{out}") + } else { + out + } +} + +pub(super) fn clean_provider_id(provider_id: &str) -> Result { + let provider_id = provider_id.trim().trim_end_matches('/').to_string(); + if provider_id.is_empty() { + return Err(InferenceProviderError::EmptyAgentProviderId); + } + if is_reserved_provider_id(&provider_id) { + return Err(InferenceProviderError::InvalidAgentProviderConfig { + agent_id: super::AGENT_ID.to_string(), + path: "config.toml".to_string(), + message: format!( + "'{provider_id}' is a built-in Codex provider id and cannot \ + be managed as a custom provider" + ), + }); + } + Ok(provider_id) +} + +pub(super) fn clean_provider_name(name: &str) -> Result { + let name = name.trim().to_string(); + if name.is_empty() { + Err(InferenceProviderError::EmptyName) + } else { + Ok(name) + } +} + +pub(super) fn ensure_api_key(api_key: &str) -> Result<()> { + if api_key.trim().is_empty() { + Err(InferenceProviderError::EmptyApiKey) + } else { + Ok(()) + } +} + +pub(super) fn ensure_responses_format( + format: Option, +) -> Result<()> { + match format { + Some(InferenceProviderFormat::OpenAiResponses) => Ok(()), + Some(other) => Err(InferenceProviderError::InvalidFormat(format!( + "Codex only supports openai_responses providers, got {other}" + ))), + None => Ok(()), + } +} + +fn format_from_table(table: &Table) -> Option { + match string_field(table, "wire_api").as_deref() { + None | Some(WIRE_API_RESPONSES) => { + Some(InferenceProviderFormat::OpenAiResponses) + } + Some(_) => None, + } +} + +fn credential_from_table(table: &Table) -> AgentProviderCredential { + if let Some(env_key) = string_field(table, "env_key") { + return AgentProviderCredential::EnvVar { name: env_key }; + } + if table.contains_key("experimental_bearer_token") + || authorization_header(table).is_some() + { + return AgentProviderCredential::Inline; + } + if table + .get("requires_openai_auth") + .and_then(Item::as_bool) + .unwrap_or(false) + || table.contains_key("auth") + { + return AgentProviderCredential::AgentStore { id: None }; + } + AgentProviderCredential::None +} + +fn authorization_header(table: &Table) -> Option<&str> { + table + .get("http_headers") + .and_then(Item::as_table_like) + .and_then(|headers| headers.get(AUTHORIZATION_HEADER)) + .and_then(Item::as_str) +} + +fn string_field(table: &Table, key: &str) -> Option { + table.get(key)?.as_str().map(ToString::to_string) +} + +fn is_reserved_provider_id(provider_id: &str) -> bool { + RESERVED_PROVIDER_IDS + .iter() + .any(|reserved| provider_id.eq_ignore_ascii_case(reserved)) +} diff --git a/crates/inference/src/codex/mod.rs b/crates/inference/src/codex/mod.rs new file mode 100644 index 00000000..0d1a1657 --- /dev/null +++ b/crates/inference/src/codex/mod.rs @@ -0,0 +1,297 @@ +//! Codex provider configuration adapter. +//! +//! Codex stores provider configuration in `config.toml`. Current Codex +//! versions only support the Responses wire API for model providers. + +mod files; +mod mapping; + +#[cfg(test)] +mod tests; + +use std::path::{Path, PathBuf}; + +use toml_edit::{value, DocumentMut, Item, Table}; + +use crate::agent::{ + AgentCredentialSupport, AgentModelSelection, AgentProviderAdapter, + AgentProviderBinding, AgentProviderCapabilities, AgentProviderCredential, + AgentProviderDefaultSupport, AgentProviderSource, AgentProviderState, + BuiltInProviderSupport, +}; +use crate::error::Result; +use crate::model::InferenceProvider; + +pub(super) const AGENT_ID: &str = "codex"; + +/// Provider adapter for Codex. +#[derive(Debug, Clone)] +pub struct CodexProviderAdapter { + config_path: PathBuf, +} + +impl CodexProviderAdapter { + /// Create an adapter with an explicit `config.toml` path. + pub fn new(config_path: impl Into) -> Self { + Self { + config_path: config_path.into(), + } + } + + /// Create an adapter for the global Codex config. + pub fn global() -> Result { + Ok(Self::new(files::default_global_config_path()?)) + } + + /// Create an adapter for a project-local `.codex/config.toml`. + pub fn for_project(project_root: impl AsRef) -> Self { + Self::new(project_root.as_ref().join(".codex/config.toml")) + } + + /// Path to the Codex config file this adapter manages. + pub fn config_path(&self) -> &Path { + &self.config_path + } + + /// Add or replace an aghub provider in Codex. + /// + /// Codex provider config is custom-provider only: built-in provider IDs + /// such as `openai`, `ollama`, and `lmstudio` are immutable. + pub fn add_provider( + &self, + provider_id: &str, + provider: &InferenceProvider, + api_key: &str, + ) -> Result { + mapping::ensure_responses_format(Some(provider.format))?; + mapping::ensure_api_key(api_key)?; + let provider_id = mapping::clean_provider_id(provider_id)?; + let binding = AgentProviderBinding::from_inventory( + provider_id.clone(), + provider, + AgentProviderCredential::Inline, + AgentProviderSource::Custom, + )?; + + let mut config = files::read_config(&self.config_path)?; + upsert_provider(&mut config, &binding, Some(api_key))?; + files::write_config(&self.config_path, &config)?; + Ok(binding) + } + + /// Add a provider using a slug derived from its stable key. + pub fn add_inventory_provider( + &self, + provider: &InferenceProvider, + api_key: &str, + ) -> Result { + let provider_id = mapping::provider_id_from_name(&provider.name); + self.add_provider(&provider_id, provider, api_key) + } + + /// Update an existing Codex provider's display name and/or API key. + pub fn update_provider( + &self, + provider_id: &str, + name: Option<&str>, + api_key: Option<&str>, + ) -> Result { + let provider_id = mapping::clean_provider_id(provider_id)?; + let mut config = files::read_config(&self.config_path)?; + let provider = provider_table(&config, &provider_id)? + .cloned() + .ok_or_else(|| { + crate::error::InferenceProviderError::NotFound( + provider_id.clone(), + ) + })?; + + let name = name.map(mapping::clean_provider_name).transpose()?; + if let Some(api_key) = api_key { + mapping::ensure_api_key(api_key)?; + } + + let mut binding = mapping::binding_from_table(&provider_id, &provider)?; + mapping::ensure_responses_format(binding.format)?; + if let Some(name) = name { + binding.name = name; + } + upsert_provider(&mut config, &binding, api_key)?; + files::write_config(&self.config_path, &config)?; + + Ok(self + .load_providers()? + .providers + .into_iter() + .find(|provider| provider.id == provider_id) + .unwrap_or(binding)) + } + + /// Read an API key visible to Codex for a provider. + pub fn api_key(&self, provider_id: &str) -> Result> { + let provider_id = mapping::clean_provider_id(provider_id)?; + let config = files::read_config(&self.config_path)?; + let Some(table) = provider_table(&config, &provider_id)? else { + return Ok(None); + }; + Ok(mapping::api_key_from_table(table)) + } + + /// Remove a custom provider definition. + pub fn remove_provider( + &self, + provider_id: &str, + ) -> Result { + let provider_id = mapping::clean_provider_id(provider_id)?; + let mut config = files::read_config(&self.config_path)?; + let removed = provider_table(&config, &provider_id)? + .map(|table| mapping::binding_from_table(&provider_id, table)) + .transpose()? + .ok_or_else(|| { + crate::error::InferenceProviderError::NotFound( + provider_id.clone(), + ) + })?; + + let providers = providers_table_mut(&mut config)?; + providers.remove(&provider_id); + if config + .get("model_provider") + .and_then(Item::as_str) + .is_some_and(|value| value == provider_id) + { + config.as_table_mut().remove("model_provider"); + } + files::write_config(&self.config_path, &config)?; + Ok(removed) + } +} + +impl AgentProviderAdapter for CodexProviderAdapter { + fn agent_id(&self) -> &'static str { + AGENT_ID + } + + fn capabilities(&self) -> AgentProviderCapabilities { + AgentProviderCapabilities::registry( + AgentProviderDefaultSupport::PROVIDER_AND_MODEL, + AgentCredentialSupport::ENV_VAR_INLINE_OR_AGENT_STORE, + BuiltInProviderSupport::IMMUTABLE, + ) + } + + fn load_providers(&self) -> Result { + let config = files::read_config(&self.config_path)?; + let mut providers = Vec::new(); + + if let Some(model_providers) = + config.get("model_providers").and_then(Item::as_table) + { + for (provider_id, item) in model_providers { + let Some(provider) = item.as_table() else { + continue; + }; + providers + .push(mapping::binding_from_table(provider_id, provider)?); + } + } + + Ok(AgentProviderState { + providers, + default_model: default_model_selection(&config), + small_model: None, + }) + } + + fn save_providers(&self, state: &AgentProviderState) -> Result<()> { + state.validate(AGENT_ID, &self.capabilities())?; + let mut config = files::read_config(&self.config_path)?; + let existing = config + .get("model_providers") + .and_then(Item::as_table) + .cloned() + .unwrap_or_default(); + let providers = providers_table_mut(&mut config)?; + providers.clear(); + + for binding in &state.providers { + if binding.source != AgentProviderSource::Custom { + continue; + } + mapping::ensure_responses_format(binding.format)?; + let existing_table = + existing.get(&binding.id).and_then(Item::as_table); + let provider = mapping::provider_table_from_binding( + binding, + None, + existing_table, + ); + providers.insert(&binding.id, Item::Table(provider)); + } + + if let Some(selection) = &state.default_model { + if let Some(provider_id) = &selection.provider_id { + config["model_provider"] = value(provider_id.clone()); + } + config["model"] = value(selection.model_id.clone()); + } + + files::write_config(&self.config_path, &config) + } +} + +fn default_model_selection( + config: &DocumentMut, +) -> Option { + let model = config.get("model")?.as_str()?.to_string(); + let provider = config + .get("model_provider") + .and_then(Item::as_str) + .map(ToString::to_string); + Some(match provider { + Some(provider) => AgentModelSelection::provider_model(provider, model), + None => AgentModelSelection::model(model), + }) +} + +fn upsert_provider( + config: &mut DocumentMut, + binding: &AgentProviderBinding, + api_key: Option<&str>, +) -> Result<()> { + mapping::ensure_responses_format(binding.format)?; + let existing = provider_table(config, &binding.id)?.cloned(); + let provider = mapping::provider_table_from_binding( + binding, + api_key, + existing.as_ref(), + ); + providers_table_mut(config)?.insert(&binding.id, Item::Table(provider)); + Ok(()) +} + +fn provider_table<'a>( + config: &'a DocumentMut, + provider_id: &str, +) -> Result> { + Ok(config + .get("model_providers") + .and_then(Item::as_table) + .and_then(|providers| providers.get(provider_id)) + .and_then(Item::as_table)) +} + +fn providers_table_mut(config: &mut DocumentMut) -> Result<&mut Table> { + if !config.as_table().contains_key("model_providers") { + config["model_providers"] = Item::Table(Table::new()); + } + config + .get_mut("model_providers") + .and_then(Item::as_table_mut) + .ok_or_else(|| { + files::invalid_config( + Path::new("config.toml"), + "`model_providers` must be a table", + ) + }) +} diff --git a/crates/inference/src/codex/tests.rs b/crates/inference/src/codex/tests.rs new file mode 100644 index 00000000..ff2ac8be --- /dev/null +++ b/crates/inference/src/codex/tests.rs @@ -0,0 +1,198 @@ +use std::fs; + +use toml_edit::{DocumentMut, Item}; + +use super::*; +use crate::agent::{AgentProviderAdapter, AgentProviderCredential}; +use crate::error::InferenceProviderError; +use crate::model::{InferenceProvider, InferenceProviderFormat}; + +fn adapter(temp: &tempfile::TempDir) -> CodexProviderAdapter { + CodexProviderAdapter::new(temp.path().join("config.toml")) +} + +fn provider() -> InferenceProvider { + InferenceProvider { + id: "inventory-id".to_string(), + name: "openrouter".to_string(), + display_name: "OpenRouter".to_string(), + format: InferenceProviderFormat::OpenAiResponses, + api_base_url: "https://openrouter.ai/api/v1".to_string(), + masked_api_key: "sk****st".to_string(), + models: vec!["openai/gpt-5.4".to_string()], + } +} + +#[test] +fn load_reads_model_providers_and_default_selection() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +model = "openai/gpt-5.4" +model_provider = "openrouter" + +[model_providers.openrouter] +name = "OpenRouter" +base_url = "https://openrouter.ai/api/v1" +wire_api = "responses" +env_key = "OPENROUTER_API_KEY" +"#, + ) + .unwrap(); + + let state = adapter.load_providers().unwrap(); + + assert_eq!(state.providers.len(), 1); + let provider = &state.providers[0]; + assert_eq!(provider.id, "openrouter"); + assert_eq!(provider.name, "OpenRouter"); + assert_eq!( + provider.format, + Some(InferenceProviderFormat::OpenAiResponses) + ); + assert_eq!( + provider.api_base_url.as_deref(), + Some("https://openrouter.ai/api/v1") + ); + assert_eq!( + provider.credential, + AgentProviderCredential::EnvVar { + name: "OPENROUTER_API_KEY".to_string() + } + ); + let default = state.default_model.unwrap(); + assert_eq!(default.provider_id.as_deref(), Some("openrouter")); + assert_eq!(default.model_id, "openai/gpt-5.4"); +} + +#[test] +fn add_provider_writes_responses_config_and_inline_token() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + + let binding = adapter + .add_provider("openrouter", &provider(), "sk-test") + .unwrap(); + + assert_eq!(binding.name, "OpenRouter"); + assert_eq!( + binding.format, + Some(InferenceProviderFormat::OpenAiResponses) + ); + let content = fs::read_to_string(adapter.config_path()).unwrap(); + let config = content.parse::().unwrap(); + let provider = config["model_providers"]["openrouter"].as_table().unwrap(); + assert_eq!(provider["name"].as_str(), Some("OpenRouter")); + assert_eq!( + provider["base_url"].as_str(), + Some("https://openrouter.ai/api/v1") + ); + assert_eq!(provider["wire_api"].as_str(), Some("responses")); + assert_eq!( + provider["experimental_bearer_token"].as_str(), + Some("sk-test") + ); +} + +#[test] +fn add_provider_rejects_chat_completion_inventory() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + let mut provider = provider(); + provider.format = InferenceProviderFormat::OpenAiCompletions; + + let error = adapter + .add_provider("openrouter", &provider, "sk-test") + .unwrap_err(); + + assert!(matches!(error, InferenceProviderError::InvalidFormat(_))); +} + +#[test] +fn update_provider_edits_name_and_token_preserving_comments() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#"# user note +model = "gpt-5.4" + +[model_providers.openrouter] +# provider note +name = "OpenRouter" +base_url = "https://openrouter.ai/api/v1" +wire_api = "responses" +experimental_bearer_token = "sk-old" +"#, + ) + .unwrap(); + + let binding = adapter + .update_provider("openrouter", Some("OpenRouter Team"), Some("sk-new")) + .unwrap(); + + assert_eq!(binding.name, "OpenRouter Team"); + let content = fs::read_to_string(adapter.config_path()).unwrap(); + assert!(content.contains("# user note")); + assert!(content.contains("# provider note")); + let config = content.parse::().unwrap(); + let provider = config["model_providers"]["openrouter"].as_table().unwrap(); + assert_eq!(provider["name"].as_str(), Some("OpenRouter Team")); + assert_eq!( + provider["experimental_bearer_token"].as_str(), + Some("sk-new") + ); +} + +#[test] +fn api_key_reads_inline_token() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +[model_providers.openrouter] +name = "OpenRouter" +base_url = "https://openrouter.ai/api/v1" +experimental_bearer_token = "sk-inline" +"#, + ) + .unwrap(); + + assert_eq!( + adapter.api_key("openrouter").unwrap(), + Some("sk-inline".to_string()) + ); +} + +#[test] +fn remove_provider_clears_default_provider() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +model = "openai/gpt-5.4" +model_provider = "openrouter" + +[model_providers.openrouter] +name = "OpenRouter" +base_url = "https://openrouter.ai/api/v1" +"#, + ) + .unwrap(); + + let removed = adapter.remove_provider("openrouter").unwrap(); + + assert_eq!(removed.id, "openrouter"); + let content = fs::read_to_string(adapter.config_path()).unwrap(); + let config = content.parse::().unwrap(); + assert!(config.get("model_provider").is_none()); + assert!(config + .get("model_providers") + .and_then(Item::as_table) + .and_then(|providers| providers.get("openrouter")) + .is_none()); +} diff --git a/crates/inference/src/lib.rs b/crates/inference/src/lib.rs index 49d9570e..9a6b13d2 100644 --- a/crates/inference/src/lib.rs +++ b/crates/inference/src/lib.rs @@ -5,6 +5,7 @@ //! keyring. pub mod agent; +pub mod codex; pub mod credentials; pub mod error; pub mod model; @@ -18,6 +19,7 @@ pub use agent::{ AgentProviderModel, AgentProviderSource, AgentProviderState, BuiltInProviderSupport, }; +pub use codex::CodexProviderAdapter; pub use credentials::{CredentialStore, NativeCredentialStore}; pub use error::{InferenceProviderError, Result}; pub use model::{ From 2c71c460680a5974c452ee482d75d99a1d1994f4 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sun, 26 Apr 2026 16:15:29 +0800 Subject: [PATCH 22/62] feat(inference): support codex profiles --- crates/api/src/bin/export-dto.rs | 10 +- crates/api/src/dto/inference.rs | 66 +- crates/api/src/lib.rs | 3 + crates/api/src/routes/inference.rs | 63 +- .../src/generated/dto/CodexProfileResponse.ts | 10 + .../dto/CodexProviderStateResponse.ts | 9 + .../dto/UpdateCodexActiveProfileRequest.ts | 3 + .../dto/UpdateCodexProfileProviderRequest.ts | 3 + crates/desktop/src/generated/dto/index.ts | 4 + crates/desktop/src/lib/api.ts | 24 + crates/desktop/src/lib/locales/en.ts | 16 + crates/desktop/src/lib/locales/zh-Hans.ts | 16 + crates/desktop/src/lib/locales/zh-Hant.ts | 16 + .../pages/inference-providers/codex-panel.tsx | 595 ++++++++++++++---- .../src/requests/inference-providers.ts | 69 ++ crates/desktop/src/requests/keys.ts | 2 + crates/inference/src/codex/mapping.rs | 22 +- crates/inference/src/codex/mod.rs | 308 ++++++++- crates/inference/src/codex/tests.rs | 173 ++++- crates/inference/src/lib.rs | 5 +- 20 files changed, 1263 insertions(+), 154 deletions(-) create mode 100644 crates/desktop/src/generated/dto/CodexProfileResponse.ts create mode 100644 crates/desktop/src/generated/dto/CodexProviderStateResponse.ts create mode 100644 crates/desktop/src/generated/dto/UpdateCodexActiveProfileRequest.ts create mode 100644 crates/desktop/src/generated/dto/UpdateCodexProfileProviderRequest.ts diff --git a/crates/api/src/bin/export-dto.rs b/crates/api/src/bin/export-dto.rs index a477e589..5d09ff36 100644 --- a/crates/api/src/bin/export-dto.rs +++ b/crates/api/src/bin/export-dto.rs @@ -16,10 +16,12 @@ use aghub_api::dto::{ AgentProviderCredentialDto, AgentProviderMatchedInferenceProviderResponse, AgentProviderModelResponse, AgentProviderResponse, - AgentProviderSourceDto, CreateAgentProviderRequest, + AgentProviderSourceDto, CodexProfileResponse, + CodexProviderStateResponse, CreateAgentProviderRequest, CreateInferenceProviderRequest, InferenceProviderFormatDto, InferenceProviderPasswordResponse, InferenceProviderResponse, - UpdateAgentProviderRequest, UpdateInferenceProviderRequest, + UpdateAgentProviderRequest, UpdateCodexActiveProfileRequest, + UpdateCodexProfileProviderRequest, UpdateInferenceProviderRequest, }, integrations::{ CodeEditorType, EditSkillFolderRequest, OpenSkillFolderRequest, @@ -127,8 +129,12 @@ fn main() -> Result<(), Box> { export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; + export_type::(&cfg)?; + export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; + export_type::(&cfg)?; + export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; diff --git a/crates/api/src/dto/inference.rs b/crates/api/src/dto/inference.rs index a37f144a..ebb34806 100644 --- a/crates/api/src/dto/inference.rs +++ b/crates/api/src/dto/inference.rs @@ -1,7 +1,8 @@ use aghub_inference::{ AgentProviderBinding, AgentProviderCredential, AgentProviderModel, - AgentProviderSource, CreateInferenceProvider, InferenceProvider, - InferenceProviderFormat, UpdateInferenceProvider, + AgentProviderSource, CodexProfileState, CodexProviderState, + CreateInferenceProvider, InferenceProvider, InferenceProviderFormat, + UpdateInferenceProvider, }; use serde::{Deserialize, Serialize}; use ts_rs::TS; @@ -229,6 +230,55 @@ pub struct AgentProviderResponse { Option, } +#[derive(Debug, Clone, Serialize, TS)] +#[ts(export)] +pub struct CodexProfileResponse { + pub id: String, + pub name: String, + pub is_default: bool, + pub is_active: bool, + pub selected_provider_id: String, + pub model: Option, +} + +impl From<&CodexProfileState> for CodexProfileResponse { + fn from(profile: &CodexProfileState) -> Self { + Self { + id: profile.id.clone(), + name: profile.name.clone(), + is_default: profile.is_default, + is_active: profile.is_active, + selected_provider_id: profile.selected_provider_id.clone(), + model: profile.model.clone(), + } + } +} + +#[derive(Debug, Clone, Serialize, TS)] +#[ts(export)] +pub struct CodexProviderStateResponse { + pub active_profile_id: String, + pub profiles: Vec, + pub providers: Vec, +} + +impl CodexProviderStateResponse { + pub fn from_state( + state: CodexProviderState, + providers: Vec, + ) -> Self { + Self { + active_profile_id: state.active_profile_id, + profiles: state + .profiles + .iter() + .map(CodexProfileResponse::from) + .collect(), + providers, + } + } +} + impl From for AgentProviderResponse { fn from(provider: AgentProviderBinding) -> Self { Self::from(&provider) @@ -278,3 +328,15 @@ pub struct UpdateAgentProviderRequest { pub name: Option, pub api_key: Option, } + +#[derive(Debug, Deserialize, TS)] +#[ts(export)] +pub struct UpdateCodexActiveProfileRequest { + pub profile_id: String, +} + +#[derive(Debug, Deserialize, TS)] +#[ts(export)] +pub struct UpdateCodexProfileProviderRequest { + pub provider_id: String, +} diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 9607819b..c873111a 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -169,10 +169,13 @@ pub async fn start(options: ApiOptions) -> Result<(), rocket::Error> { routes::inference::list_inference_providers, routes::inference::list_opencode_providers, routes::inference::list_codex_providers, + routes::inference::get_codex_state, routes::inference::create_opencode_provider, routes::inference::create_codex_provider, routes::inference::update_opencode_provider, routes::inference::update_codex_provider, + routes::inference::update_codex_active_profile, + routes::inference::update_codex_profile_provider, routes::inference::sync_opencode_provider, routes::inference::sync_codex_provider, routes::inference::delete_opencode_provider, diff --git a/crates/api/src/routes/inference.rs b/crates/api/src/routes/inference.rs index 06e18d20..e594fd65 100644 --- a/crates/api/src/routes/inference.rs +++ b/crates/api/src/routes/inference.rs @@ -9,10 +9,11 @@ use rocket::serde::json::Json; use rocket::State; use crate::dto::inference::{ - AgentProviderResponse, CreateAgentProviderRequest, - CreateInferenceProviderRequest, InferenceProviderPasswordResponse, - InferenceProviderResponse, UpdateAgentProviderRequest, - UpdateInferenceProviderRequest, + AgentProviderResponse, CodexProviderStateResponse, + CreateAgentProviderRequest, CreateInferenceProviderRequest, + InferenceProviderPasswordResponse, InferenceProviderResponse, + UpdateAgentProviderRequest, UpdateCodexActiveProfileRequest, + UpdateCodexProfileProviderRequest, UpdateInferenceProviderRequest, }; use crate::error::{ApiCreated, ApiError, ApiNoContent, ApiResult}; use crate::state::InferenceProviderState; @@ -145,6 +146,21 @@ fn codex_provider_response( }) } +fn codex_state_response( + store: &InferenceProviderStore, + adapter: &CodexProviderAdapter, +) -> Result { + let inventory = inventory_providers_with_api_keys(store)?; + let state = adapter.load_profile_state().map_err(ApiError::from)?; + let providers = state + .providers + .iter() + .cloned() + .map(|binding| codex_provider_response(&inventory, adapter, binding)) + .collect::, _>>()?; + Ok(CodexProviderStateResponse::from_state(state, providers)) +} + #[get("/inference/providers")] pub fn list_inference_providers( state: &State, @@ -194,6 +210,15 @@ pub fn list_codex_providers( Ok(Json(providers)) } +#[get("/inference/agents/codex/state")] +pub fn get_codex_state( + state: &State, +) -> ApiResult { + let store = store(state); + let adapter = codex_adapter()?; + Ok(Json(codex_state_response(&store, &adapter)?)) +} + #[post("/inference/agents/opencode/providers", data = "")] pub fn create_opencode_provider( state: &State, @@ -250,6 +275,36 @@ pub fn update_codex_provider( Ok(Json(binding.into())) } +#[put("/inference/agents/codex/profile", data = "")] +pub fn update_codex_active_profile( + state: &State, + body: Json, +) -> ApiResult { + let store = store(state); + let adapter = codex_adapter()?; + adapter + .set_active_profile(&body.profile_id) + .map_err(ApiError::from)?; + Ok(Json(codex_state_response(&store, &adapter)?)) +} + +#[put( + "/inference/agents/codex/profiles//provider", + data = "" +)] +pub fn update_codex_profile_provider( + state: &State, + profile_id: &str, + body: Json, +) -> ApiResult { + let store = store(state); + let adapter = codex_adapter()?; + adapter + .set_profile_provider(profile_id, &body.provider_id) + .map_err(ApiError::from)?; + Ok(Json(codex_state_response(&store, &adapter)?)) +} + #[post("/inference/agents/opencode/providers//sync")] pub fn sync_opencode_provider( state: &State, diff --git a/crates/desktop/src/generated/dto/CodexProfileResponse.ts b/crates/desktop/src/generated/dto/CodexProfileResponse.ts new file mode 100644 index 00000000..186189cd --- /dev/null +++ b/crates/desktop/src/generated/dto/CodexProfileResponse.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CodexProfileResponse = { + id: string; + name: string; + is_default: boolean; + is_active: boolean; + selected_provider_id: string; + model: string | null; +}; diff --git a/crates/desktop/src/generated/dto/CodexProviderStateResponse.ts b/crates/desktop/src/generated/dto/CodexProviderStateResponse.ts new file mode 100644 index 00000000..e5277efa --- /dev/null +++ b/crates/desktop/src/generated/dto/CodexProviderStateResponse.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AgentProviderResponse } from "./AgentProviderResponse"; +import type { CodexProfileResponse } from "./CodexProfileResponse"; + +export type CodexProviderStateResponse = { + active_profile_id: string; + profiles: Array; + providers: Array; +}; diff --git a/crates/desktop/src/generated/dto/UpdateCodexActiveProfileRequest.ts b/crates/desktop/src/generated/dto/UpdateCodexActiveProfileRequest.ts new file mode 100644 index 00000000..0a0555a6 --- /dev/null +++ b/crates/desktop/src/generated/dto/UpdateCodexActiveProfileRequest.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type UpdateCodexActiveProfileRequest = { profile_id: string }; diff --git a/crates/desktop/src/generated/dto/UpdateCodexProfileProviderRequest.ts b/crates/desktop/src/generated/dto/UpdateCodexProfileProviderRequest.ts new file mode 100644 index 00000000..4a149ba6 --- /dev/null +++ b/crates/desktop/src/generated/dto/UpdateCodexProfileProviderRequest.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type UpdateCodexProfileProviderRequest = { provider_id: string }; diff --git a/crates/desktop/src/generated/dto/index.ts b/crates/desktop/src/generated/dto/index.ts index 41879278..099f40a6 100644 --- a/crates/desktop/src/generated/dto/index.ts +++ b/crates/desktop/src/generated/dto/index.ts @@ -7,6 +7,8 @@ export type { AgentProviderResponse } from "./AgentProviderResponse"; export type { AgentProviderSourceDto } from "./AgentProviderSourceDto"; export type { CapabilitiesDto } from "./CapabilitiesDto"; export type { CodeEditorType } from "./CodeEditorType"; +export type { CodexProfileResponse } from "./CodexProfileResponse"; +export type { CodexProviderStateResponse } from "./CodexProviderStateResponse"; export type { ConfigSource } from "./ConfigSource"; export type { CreateAgentProviderRequest } from "./CreateAgentProviderRequest"; export type { CreateCredentialRequest } from "./CreateCredentialRequest"; @@ -64,6 +66,8 @@ export type { ToolPreferencesDto } from "./ToolPreferencesDto"; export type { TransferRequest } from "./TransferRequest"; export type { TransportDto } from "./TransportDto"; export type { UpdateAgentProviderRequest } from "./UpdateAgentProviderRequest"; +export type { UpdateCodexActiveProfileRequest } from "./UpdateCodexActiveProfileRequest"; +export type { UpdateCodexProfileProviderRequest } from "./UpdateCodexProfileProviderRequest"; export type { UpdateInferenceProviderRequest } from "./UpdateInferenceProviderRequest"; export type { UpdateMcpRequest } from "./UpdateMcpRequest"; export type { UpdateSkillRequest } from "./UpdateSkillRequest"; diff --git a/crates/desktop/src/lib/api.ts b/crates/desktop/src/lib/api.ts index 239c94cd..6b04cf05 100644 --- a/crates/desktop/src/lib/api.ts +++ b/crates/desktop/src/lib/api.ts @@ -4,6 +4,7 @@ import type { AgentInfo, AgentProviderResponse, CodeEditorType, + CodexProviderStateResponse, CreateAgentProviderRequest, CreateCredentialRequest, CreateInferenceProviderRequest, @@ -36,6 +37,8 @@ import type { ToolInfoDto, TransferRequest, UpdateAgentProviderRequest, + UpdateCodexActiveProfileRequest, + UpdateCodexProfileProviderRequest, UpdateInferenceProviderRequest, UpdateMcpRequest, UpdateSubAgentRequest, @@ -492,6 +495,9 @@ export function createApi(baseUrl: string) { listCodex(): Promise { return client.get("inference/agents/codex/providers").json(); }, + getCodexState(): Promise { + return client.get("inference/agents/codex/state").json(); + }, getPassword( name: string, ): Promise { @@ -554,6 +560,24 @@ export function createApi(baseUrl: string) { ) .json(); }, + updateCodexActiveProfile( + body: UpdateCodexActiveProfileRequest, + ): Promise { + return client + .put("inference/agents/codex/profile", { json: body }) + .json(); + }, + updateCodexProfileProvider( + profileId: string, + body: UpdateCodexProfileProviderRequest, + ): Promise { + return client + .put( + `inference/agents/codex/profiles/${encodeURIComponent(profileId)}/provider`, + { json: body }, + ) + .json(); + }, syncOpenCode(id: string): Promise { return client .post( diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index 8b004c41..5da2a31a 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -134,6 +134,22 @@ export default { codexBuiltInProvider: "Built-in provider", codexBuiltInProviderInfo: "No Aghub OpenAI Responses provider has the same API Base URL and API key. This was likely added directly in Codex.", + codexActiveProfile: "Active Profile", + codexProfileProvider: "Profile Provider", + codexCurrentRoute: "Current Route", + codexActiveProvider: "Active", + codexLoginProvider: "Codex login", + codexLoginProviderInfo: + "Uses Codex's own OpenAI login credentials without writing a base URL or API key to config.toml.", + codexConfigProvider: "Config provider", + codexConfigProviderInfo: + "This provider comes from Codex config.toml and does not currently match an Aghub inference provider.", + codexCustomProviders: "Custom Providers", + noCodexCustomProviders: "No custom Codex providers configured.", + codexNoProfiles: "No Codex profiles configured.", + codexProfileUpdateError: "Failed to switch Codex profile", + codexProfileProviderUpdated: "Codex profile provider updated", + codexProfileProviderUpdateError: "Failed to update Codex profile provider", syncCodexProvider: "Update Codex provider", syncCodexProviderFromInferenceProvider: "Update provider config from {{name}}", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index 4bd9e42e..a155235d 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -129,6 +129,22 @@ export default { codexBuiltInProvider: "内置 provider", codexBuiltInProviderInfo: "未在 Aghub OpenAI Responses Provider 中找到相同 API Base URL 和 API key,疑似用户直接在 Codex 里添加。", + codexActiveProfile: "当前 Profile", + codexProfileProvider: "Profile Provider", + codexCurrentRoute: "当前路由", + codexActiveProvider: "当前", + codexLoginProvider: "Codex 登录", + codexLoginProviderInfo: + "使用 Codex 自己的 OpenAI 登录凭据,不在 config.toml 写入 base URL 或 API key。", + codexConfigProvider: "配置文件 provider", + codexConfigProviderInfo: + "这个 provider 来自 Codex config.toml,暂未匹配到 Aghub 推理 Provider。", + codexCustomProviders: "自定义 Provider", + noCodexCustomProviders: "暂无自定义 Codex Provider。", + codexNoProfiles: "暂无 Codex profile。", + codexProfileUpdateError: "切换 Codex Profile 失败", + codexProfileProviderUpdated: "Codex Profile Provider 已更新", + codexProfileProviderUpdateError: "更新 Codex Profile Provider 失败", syncCodexProvider: "更新 Codex Provider", syncCodexProviderFromInferenceProvider: "从 {{name}} 更新 Provider 配置", agentProviderSourceCustom: "自定义", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index 1984c033..33a375fa 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -129,6 +129,22 @@ export default { codexBuiltInProvider: "內建 provider", codexBuiltInProviderInfo: "未在 Aghub OpenAI Responses Provider 中找到相同 API Base URL 和 API key,疑似使用者直接在 Codex 裡新增。", + codexActiveProfile: "目前 Profile", + codexProfileProvider: "Profile Provider", + codexCurrentRoute: "目前路由", + codexActiveProvider: "目前", + codexLoginProvider: "Codex 登入", + codexLoginProviderInfo: + "使用 Codex 自己的 OpenAI 登入憑據,不在 config.toml 寫入 base URL 或 API key。", + codexConfigProvider: "設定檔 provider", + codexConfigProviderInfo: + "這個 provider 來自 Codex config.toml,暫未匹配到 Aghub 推理 Provider。", + codexCustomProviders: "自訂 Provider", + noCodexCustomProviders: "暫無自訂 Codex Provider。", + codexNoProfiles: "暫無 Codex profile。", + codexProfileUpdateError: "切換 Codex Profile 失敗", + codexProfileProviderUpdated: "Codex Profile Provider 已更新", + codexProfileProviderUpdateError: "更新 Codex Profile Provider 失敗", syncCodexProvider: "更新 Codex Provider", syncCodexProviderFromInferenceProvider: "從 {{name}} 更新 Provider 配置", agentProviderSourceCustom: "自訂", diff --git a/crates/desktop/src/pages/inference-providers/codex-panel.tsx b/crates/desktop/src/pages/inference-providers/codex-panel.tsx index 70c508c5..c68b17ff 100644 --- a/crates/desktop/src/pages/inference-providers/codex-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/codex-panel.tsx @@ -1,5 +1,7 @@ import { ArrowPathIcon, + CheckCircleIcon, + KeyIcon, PencilIcon, PlusIcon, QuestionMarkCircleIcon, @@ -34,11 +36,13 @@ import { useApi } from "../../hooks/use-api"; import { AgentIcon } from "../../lib/agent-icons"; import { cn } from "../../lib/utils"; import { - codexProviderListQueryOptions, + codexProviderStateQueryOptions, createCodexProviderMutationOptions, deleteCodexProviderMutationOptions, inferenceProviderListQueryOptions, syncCodexProviderMutationOptions, + updateCodexActiveProfileMutationOptions, + updateCodexProfileProviderMutationOptions, updateCodexProviderMutationOptions, } from "../../requests/inference-providers"; @@ -357,7 +361,98 @@ function CodexEditProviderDialog({ ); } -function ProviderRow({ +function ProviderIdentity({ + provider, + fallbackId, + isActive, +}: { + provider: AgentProviderResponse | undefined; + fallbackId: string; + isActive?: boolean; +}) { + const { t } = useTranslation(); + const isLoginProvider = provider?.source === "built_in"; + const label = provider?.name ?? fallbackId; + + return ( +
+ {isLoginProvider ? ( + + ) : ( + + )} + + {provider && provider.id !== provider.name && ( + + {provider.id} + + )} + {isActive && ( + + + {t("codexActiveProvider")} + + )} +
+ ); +} + +function ProviderMeta({ provider }: { provider: AgentProviderResponse }) { + const { t } = useTranslation(); + const matchedProvider = provider.matched_inference_provider; + + if (provider.source === "built_in") { + return ( + + {t("codexLoginProvider")} + + + + + + + + {t("codexLoginProviderInfo")} + + + + ); + } + + if (matchedProvider) { + return ( + + {t("providerModels")}: {matchedProvider.model_count} + + ); + } + + return ( + + {t("codexConfigProvider")} + + + + + + + + {t("codexConfigProviderInfo")} + + + + ); +} + +function ProviderActions({ provider, isSyncing, onEdit, @@ -372,104 +467,112 @@ function ProviderRow({ }) { const { t } = useTranslation(); const matchedProvider = provider.matched_inference_provider; + const isBuiltIn = provider.source === "built_in"; - return ( -
-
-
- - - {provider.id !== provider.name && ( - - {provider.id} - - )} -
-
- {matchedProvider ? ( - - {t("providerModels")}: {matchedProvider.model_count} - - ) : ( - - {t("codexBuiltInProvider")} - - - - - - - - {t("codexBuiltInProviderInfo")} - - - - )} -
-
+ if (isBuiltIn) { + return null; + } -
- {matchedProvider && ( - - - - - - {t("syncCodexProviderFromInferenceProvider", { - name: matchedProvider.display_name, - })} - - - )} - - - - - {t("edit")} - + return ( +
+ {matchedProvider && ( - {t("delete")} + + {t("syncCodexProviderFromInferenceProvider", { + name: matchedProvider.display_name, + })} + + )} + + + + + {t("edit")} + + + + + + {t("delete")} + +
+ ); +} + +function ProviderRow({ + provider, + isActive, + isSyncing, + onEdit, + onSync, + onDelete, +}: { + provider: AgentProviderResponse; + isActive: boolean; + isSyncing: boolean; + onEdit: () => void; + onSync: () => void; + onDelete: () => void; +}) { + return ( +
+
+ +
+ + {provider.api_base_url && ( + + {provider.api_base_url} + + )} +
+ +
); } @@ -488,18 +591,63 @@ export function CodexInferenceProviderPanel({ useState(null); const { - data: providers = [], + data: codexState, isLoading, isFetching, refetch, } = useQuery({ - ...codexProviderListQueryOptions({ api }), + ...codexProviderStateQueryOptions({ api }), }); const { data: inventoryProviders = [], isLoading: isInventoryLoading } = useQuery({ ...inferenceProviderListQueryOptions({ api }), }); + const providers = codexState?.providers ?? []; + const profiles = codexState?.profiles ?? []; + const activeProfile = + profiles.find((profile) => profile.is_active) ?? profiles[0]; + const activeProvider = providers.find( + (provider) => provider.id === activeProfile?.selected_provider_id, + ); + const customProviders = providers.filter( + (provider) => provider.source !== "built_in", + ); + const selectableProviderIds = new Set( + providers.map((provider) => provider.id), + ); + + const profileMutation = useMutation({ + ...updateCodexActiveProfileMutationOptions({ + api, + queryClient, + }), + onError: (error) => { + console.error("Failed to update Codex profile:", error); + toast.danger( + error instanceof Error + ? error.message + : t("codexProfileUpdateError"), + ); + }, + }); + const profileProviderMutation = useMutation({ + ...updateCodexProfileProviderMutationOptions({ + api, + queryClient, + onSuccess: async () => { + toast.success(t("codexProfileProviderUpdated")); + }, + }), + onError: (error) => { + console.error("Failed to update Codex profile provider:", error); + toast.danger( + error instanceof Error + ? error.message + : t("codexProfileProviderUpdateError"), + ); + }, + }); const deleteMutation = useMutation({ ...deleteCodexProviderMutationOptions({ api, @@ -546,6 +694,14 @@ export function CodexInferenceProviderPanel({ setProviderDialog({ type: "edit", provider }); }; + const handleProfileProviderChange = (providerId: string) => { + if (!activeProfile) return; + profileProviderMutation.mutate({ + profileId: activeProfile.id, + body: { provider_id: providerId }, + }); + }; + return ( <>
@@ -603,20 +759,194 @@ export function CodexInferenceProviderPanel({
- + {isLoading ? (
) : ( <> - {providers.length === 0 ? ( -
-

- {t("noCodexProviders")} -

+
+
+ + {t("codexCurrentRoute")} + + {activeProfile ? ( + <> + +
+ {activeProvider && ( + + )} + {activeProfile.model && ( + + {t( + "providerModels", + )} + :{" "} + { + activeProfile.model + } + + )} + {activeProvider?.api_base_url && ( + + { + activeProvider.api_base_url + } + + )} +
+ + ) : ( +

+ {t("codexNoProfiles")} +

+ )} +
+ +
+ + + +
+
+ +
+
+
- ) : ( -
- {providers.map((provider) => ( - - handleEditProvider( - provider, - ) - } - onSync={() => - syncMutation.mutate( - provider.id, - ) - } - onDelete={() => - setDeleteTarget( - provider, - ) - } - /> - ))} -
- )} + {customProviders.length === 0 ? ( +
+

+ {t( + "noCodexCustomProviders", + )} +

+
+ ) : ( +
+ {customProviders.map( + (provider) => ( + + handleEditProvider( + provider, + ) + } + onSync={() => + syncMutation.mutate( + provider.id, + ) + } + onDelete={() => + setDeleteTarget( + provider, + ) + } + /> + ), + )} +
+ )} +
)} diff --git a/crates/desktop/src/requests/inference-providers.ts b/crates/desktop/src/requests/inference-providers.ts index 8f81bb16..1f664b4a 100644 --- a/crates/desktop/src/requests/inference-providers.ts +++ b/crates/desktop/src/requests/inference-providers.ts @@ -5,10 +5,13 @@ import { } from "@tanstack/react-query"; import type { AgentProviderResponse, + CodexProviderStateResponse, CreateAgentProviderRequest, CreateInferenceProviderRequest, InferenceProviderResponse, UpdateAgentProviderRequest, + UpdateCodexActiveProfileRequest, + UpdateCodexProfileProviderRequest, UpdateInferenceProviderRequest, } from "../generated/dto"; import type { ApiClient } from "./client"; @@ -59,6 +62,19 @@ export function codexProviderListQueryOptions({ }); } +export function codexProviderStateQueryOptions({ + api, + enabled = true, + staleTime = 30_000, +}: InferenceProviderListQueryParams) { + return queryOptions({ + queryKey: queryKeys.inferenceProviders.agentState("codex"), + queryFn: () => api.inferenceProviders.getCodexState(), + enabled, + staleTime, + }); +} + export async function invalidateInferenceProviderQueries( queryClient: QueryClient, ) { @@ -243,6 +259,59 @@ export function updateCodexProviderMutationOptions({ }); } +interface UpdateCodexActiveProfileMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: (data: CodexProviderStateResponse) => void | Promise; +} + +export function updateCodexActiveProfileMutationOptions({ + api, + queryClient, + onSuccess, +}: UpdateCodexActiveProfileMutationParams) { + return mutationOptions({ + mutationFn: (body: UpdateCodexActiveProfileRequest) => + api.inferenceProviders.updateCodexActiveProfile(body), + onSuccess: async (data) => { + await invalidateCodexProviderQueries(queryClient); + await onSuccess?.(data); + }, + }); +} + +interface UpdateCodexProfileProviderVariables { + profileId: string; + body: UpdateCodexProfileProviderRequest; +} + +interface UpdateCodexProfileProviderMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: ( + data: CodexProviderStateResponse, + variables: UpdateCodexProfileProviderVariables, + ) => void | Promise; +} + +export function updateCodexProfileProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: UpdateCodexProfileProviderMutationParams) { + return mutationOptions({ + mutationFn: ({ + profileId, + body, + }: UpdateCodexProfileProviderVariables) => + api.inferenceProviders.updateCodexProfileProvider(profileId, body), + onSuccess: async (data, variables) => { + await invalidateCodexProviderQueries(queryClient); + await onSuccess?.(data, variables); + }, + }); +} + interface SyncAgentProviderMutationParams { api: ApiClient; queryClient: QueryClient; diff --git a/crates/desktop/src/requests/keys.ts b/crates/desktop/src/requests/keys.ts index 708eada3..c5cae54d 100644 --- a/crates/desktop/src/requests/keys.ts +++ b/crates/desktop/src/requests/keys.ts @@ -54,6 +54,8 @@ export const queryKeys = { list: () => ["inference-providers", "list"] as const, agent: (agentId: string) => ["inference-providers", "agent", agentId] as const, + agentState: (agentId: string) => + ["inference-providers", "agent", agentId, "state"] as const, password: (name: string) => ["inference-providers", "password", name] as const, }, diff --git a/crates/inference/src/codex/mapping.rs b/crates/inference/src/codex/mapping.rs index 646120f8..d8a31dd0 100644 --- a/crates/inference/src/codex/mapping.rs +++ b/crates/inference/src/codex/mapping.rs @@ -9,9 +9,27 @@ use crate::agent::{ use crate::error::{InferenceProviderError, Result}; use crate::model::InferenceProviderFormat; +pub(super) const OPENAI_PROVIDER_ID: &str = "openai"; + const WIRE_API_RESPONSES: &str = "responses"; const AUTHORIZATION_HEADER: &str = "Authorization"; -const RESERVED_PROVIDER_IDS: &[&str] = &["openai", "ollama", "lmstudio"]; +const RESERVED_PROVIDER_IDS: &[&str] = + &[OPENAI_PROVIDER_ID, "ollama", "lmstudio"]; + +pub(super) fn built_in_openai_binding() -> AgentProviderBinding { + AgentProviderBinding { + id: OPENAI_PROVIDER_ID.to_string(), + source_provider_id: None, + name: "OpenAI".to_string(), + format: Some(InferenceProviderFormat::OpenAiResponses), + api_base_url: None, + credential: AgentProviderCredential::AgentStore { + id: Some(OPENAI_PROVIDER_ID.to_string()), + }, + models: Vec::::new(), + source: AgentProviderSource::BuiltIn, + } +} pub(super) fn binding_from_table( provider_id: &str, @@ -185,7 +203,7 @@ fn string_field(table: &Table, key: &str) -> Option { table.get(key)?.as_str().map(ToString::to_string) } -fn is_reserved_provider_id(provider_id: &str) -> bool { +pub(super) fn is_reserved_provider_id(provider_id: &str) -> bool { RESERVED_PROVIDER_IDS .iter() .any(|reserved| provider_id.eq_ignore_ascii_case(reserved)) diff --git a/crates/inference/src/codex/mod.rs b/crates/inference/src/codex/mod.rs index 0d1a1657..28432169 100644 --- a/crates/inference/src/codex/mod.rs +++ b/crates/inference/src/codex/mod.rs @@ -23,6 +23,7 @@ use crate::error::Result; use crate::model::InferenceProvider; pub(super) const AGENT_ID: &str = "codex"; +pub const DEFAULT_PROFILE_ID: &str = "default"; /// Provider adapter for Codex. #[derive(Debug, Clone)] @@ -30,6 +31,36 @@ pub struct CodexProviderAdapter { config_path: PathBuf, } +/// Effective provider selection for one Codex profile. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CodexProfileState { + /// Stable UI/API id. The implicit top-level profile uses `default`. + pub id: String, + + /// User-facing label. + pub name: String, + + /// Whether this is the implicit top-level config profile. + pub is_default: bool, + + /// Whether Codex currently selects this profile. + pub is_active: bool, + + /// Effective provider id after profile overrides and top-level fallback. + pub selected_provider_id: String, + + /// Effective model after profile overrides and top-level fallback. + pub model: Option, +} + +/// Codex-specific provider state, including profile routing. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CodexProviderState { + pub active_profile_id: String, + pub profiles: Vec, + pub providers: Vec, +} + impl CodexProviderAdapter { /// Create an adapter with an explicit `config.toml` path. pub fn new(config_path: impl Into) -> Self { @@ -53,6 +84,70 @@ impl CodexProviderAdapter { &self.config_path } + /// Load Codex providers with effective profile selection. + pub fn load_profile_state(&self) -> Result { + let config = files::read_config(&self.config_path)?; + let providers = providers_from_config(&config)?; + let active_profile_id = active_profile_id(&config); + let profiles = profile_ids(&config, &active_profile_id) + .into_iter() + .map(|profile_id| profile_state(&config, &profile_id)) + .collect(); + + Ok(CodexProviderState { + active_profile_id, + profiles, + providers, + }) + } + + /// Select the active Codex profile. + pub fn set_active_profile( + &self, + profile_id: &str, + ) -> Result { + let profile_id = clean_profile_id(profile_id)?; + let mut config = files::read_config(&self.config_path)?; + ensure_profile_exists(&config, &profile_id)?; + + if profile_id == DEFAULT_PROFILE_ID { + config.as_table_mut().remove("profile"); + } else { + config["profile"] = value(profile_id); + } + + files::write_config(&self.config_path, &config)?; + self.load_profile_state() + } + + /// Set the provider used by one Codex profile. + pub fn set_profile_provider( + &self, + profile_id: &str, + provider_id: &str, + ) -> Result { + let profile_id = clean_profile_id(profile_id)?; + let provider_id = clean_selected_provider_id(provider_id)?; + let mut config = files::read_config(&self.config_path)?; + ensure_profile_exists(&config, &profile_id)?; + ensure_selectable_provider(&config, &provider_id)?; + + if profile_id == DEFAULT_PROFILE_ID { + let table = config.as_table_mut(); + if provider_id == mapping::OPENAI_PROVIDER_ID { + table.remove("model_provider"); + } else { + table["model_provider"] = value(provider_id); + } + } else { + let profile = profile_table_mut(&mut config, &profile_id)?; + profile["model_provider"] = value(provider_id); + } + + files::write_config(&self.config_path, &config)?; + self.load_profile_state() + } + /// Add or replace an aghub provider in Codex. /// /// Codex provider config is custom-provider only: built-in provider IDs @@ -129,7 +224,11 @@ impl CodexProviderAdapter { /// Read an API key visible to Codex for a provider. pub fn api_key(&self, provider_id: &str) -> Result> { - let provider_id = mapping::clean_provider_id(provider_id)?; + let provider_id = provider_id.trim().trim_end_matches('/').to_string(); + if mapping::is_reserved_provider_id(&provider_id) { + return Ok(None); + } + let provider_id = mapping::clean_provider_id(&provider_id)?; let config = files::read_config(&self.config_path)?; let Some(table) = provider_table(&config, &provider_id)? else { return Ok(None); @@ -162,6 +261,7 @@ impl CodexProviderAdapter { { config.as_table_mut().remove("model_provider"); } + clear_profile_provider_references(&mut config, &provider_id); files::write_config(&self.config_path, &config)?; Ok(removed) } @@ -182,22 +282,9 @@ impl AgentProviderAdapter for CodexProviderAdapter { fn load_providers(&self) -> Result { let config = files::read_config(&self.config_path)?; - let mut providers = Vec::new(); - - if let Some(model_providers) = - config.get("model_providers").and_then(Item::as_table) - { - for (provider_id, item) in model_providers { - let Some(provider) = item.as_table() else { - continue; - }; - providers - .push(mapping::binding_from_table(provider_id, provider)?); - } - } Ok(AgentProviderState { - providers, + providers: providers_from_config(&config)?, default_model: default_model_selection(&config), small_model: None, }) @@ -240,6 +327,87 @@ impl AgentProviderAdapter for CodexProviderAdapter { } } +fn providers_from_config( + config: &DocumentMut, +) -> Result> { + let mut providers = vec![mapping::built_in_openai_binding()]; + + if let Some(model_providers) = + config.get("model_providers").and_then(Item::as_table) + { + for (provider_id, item) in model_providers { + let Some(provider) = item.as_table() else { + continue; + }; + providers.push(mapping::binding_from_table(provider_id, provider)?); + } + } + + Ok(providers) +} + +fn active_profile_id(config: &DocumentMut) -> String { + config + .get("profile") + .and_then(Item::as_str) + .map(ToString::to_string) + .unwrap_or_else(|| DEFAULT_PROFILE_ID.to_string()) +} + +fn profile_ids(config: &DocumentMut, active_profile_id: &str) -> Vec { + let mut ids = vec![DEFAULT_PROFILE_ID.to_string()]; + if let Some(profiles) = config.get("profiles").and_then(Item::as_table) { + for (profile_id, item) in profiles { + if item.as_table().is_some() + && profile_id != DEFAULT_PROFILE_ID + && !ids.iter().any(|id| id == profile_id) + { + ids.push(profile_id.to_string()); + } + } + } + if active_profile_id != DEFAULT_PROFILE_ID + && !ids.iter().any(|id| id == active_profile_id) + { + ids.push(active_profile_id.to_string()); + } + ids +} + +fn profile_state(config: &DocumentMut, profile_id: &str) -> CodexProfileState { + let is_default = profile_id == DEFAULT_PROFILE_ID; + let active_profile_id = active_profile_id(config); + CodexProfileState { + id: profile_id.to_string(), + name: if is_default { + "Default".to_string() + } else { + profile_id.to_string() + }, + is_default, + is_active: active_profile_id == profile_id, + selected_provider_id: effective_profile_value( + config, + profile_id, + "model_provider", + ) + .unwrap_or_else(|| mapping::OPENAI_PROVIDER_ID.to_string()), + model: effective_profile_value(config, profile_id, "model"), + } +} + +fn effective_profile_value( + config: &DocumentMut, + profile_id: &str, + key: &str, +) -> Option { + named_profile_table(config, profile_id) + .and_then(|profile| profile.get(key)) + .and_then(Item::as_str) + .or_else(|| config.get(key).and_then(Item::as_str)) + .map(ToString::to_string) +} + fn default_model_selection( config: &DocumentMut, ) -> Option { @@ -295,3 +463,113 @@ fn providers_table_mut(config: &mut DocumentMut) -> Result<&mut Table> { ) }) } + +fn named_profile_table<'a>( + config: &'a DocumentMut, + profile_id: &str, +) -> Option<&'a Table> { + if profile_id == DEFAULT_PROFILE_ID { + return None; + } + config + .get("profiles") + .and_then(Item::as_table) + .and_then(|profiles| profiles.get(profile_id)) + .and_then(Item::as_table) +} + +fn profile_table_mut<'a>( + config: &'a mut DocumentMut, + profile_id: &str, +) -> Result<&'a mut Table> { + let Some(profiles) = config.get_mut("profiles") else { + return Err(crate::error::InferenceProviderError::NotFound(format!( + "codex profile {profile_id}" + ))); + }; + let profiles = profiles.as_table_mut().ok_or_else(|| { + files::invalid_config( + Path::new("config.toml"), + "`profiles` must be a table", + ) + })?; + let Some(profile) = profiles.get_mut(profile_id) else { + return Err(crate::error::InferenceProviderError::NotFound(format!( + "codex profile {profile_id}" + ))); + }; + profile.as_table_mut().ok_or_else(|| { + files::invalid_config( + Path::new("config.toml"), + format!("`profiles.{profile_id}` must be a table"), + ) + }) +} + +fn clean_profile_id(profile_id: &str) -> Result { + let profile_id = profile_id.trim().to_string(); + if profile_id.is_empty() { + Err(crate::error::InferenceProviderError::EmptyAgentProviderId) + } else { + Ok(profile_id) + } +} + +fn clean_selected_provider_id(provider_id: &str) -> Result { + let provider_id = provider_id.trim().trim_end_matches('/').to_string(); + if provider_id.is_empty() { + Err(crate::error::InferenceProviderError::EmptyAgentProviderId) + } else { + Ok(provider_id) + } +} + +fn ensure_profile_exists(config: &DocumentMut, profile_id: &str) -> Result<()> { + if profile_id == DEFAULT_PROFILE_ID + || named_profile_table(config, profile_id).is_some() + { + Ok(()) + } else { + Err(crate::error::InferenceProviderError::NotFound(format!( + "codex profile {profile_id}" + ))) + } +} + +fn ensure_selectable_provider( + config: &DocumentMut, + provider_id: &str, +) -> Result<()> { + if provider_id.eq_ignore_ascii_case(mapping::OPENAI_PROVIDER_ID) + || provider_table(config, provider_id)?.is_some() + { + Ok(()) + } else { + Err(crate::error::InferenceProviderError::NotFound( + provider_id.to_string(), + )) + } +} + +fn clear_profile_provider_references( + config: &mut DocumentMut, + provider_id: &str, +) { + let Some(profiles) = + config.get_mut("profiles").and_then(Item::as_table_mut) + else { + return; + }; + for (_, item) in profiles.iter_mut() { + let Some(profile) = item.as_table_mut() else { + continue; + }; + if profile + .get("model_provider") + .and_then(Item::as_str) + .is_some_and(|value| value == provider_id) + { + profile.remove("model_provider"); + } + } +} diff --git a/crates/inference/src/codex/tests.rs b/crates/inference/src/codex/tests.rs index ff2ac8be..56ac03ed 100644 --- a/crates/inference/src/codex/tests.rs +++ b/crates/inference/src/codex/tests.rs @@ -3,7 +3,9 @@ use std::fs; use toml_edit::{DocumentMut, Item}; use super::*; -use crate::agent::{AgentProviderAdapter, AgentProviderCredential}; +use crate::agent::{ + AgentProviderAdapter, AgentProviderCredential, AgentProviderSource, +}; use crate::error::InferenceProviderError; use crate::model::{InferenceProvider, InferenceProviderFormat}; @@ -44,8 +46,12 @@ env_key = "OPENROUTER_API_KEY" let state = adapter.load_providers().unwrap(); - assert_eq!(state.providers.len(), 1); - let provider = &state.providers[0]; + assert_eq!(state.providers.len(), 2); + let provider = state + .providers + .iter() + .find(|provider| provider.id == "openrouter") + .unwrap(); assert_eq!(provider.id, "openrouter"); assert_eq!(provider.name, "OpenRouter"); assert_eq!( @@ -67,6 +73,159 @@ env_key = "OPENROUTER_API_KEY" assert_eq!(default.model_id, "openai/gpt-5.4"); } +#[test] +fn profile_state_defaults_to_openai_login() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write(adapter.config_path(), r#"model = "gpt-5.4""#).unwrap(); + + let state = adapter.load_profile_state().unwrap(); + + assert_eq!(state.active_profile_id, DEFAULT_PROFILE_ID); + assert_eq!(state.providers.len(), 1); + let openai = &state.providers[0]; + assert_eq!(openai.id, "openai"); + assert_eq!(openai.source, AgentProviderSource::BuiltIn); + assert_eq!(openai.api_base_url, None); + assert_eq!( + openai.credential, + AgentProviderCredential::AgentStore { + id: Some("openai".to_string()) + } + ); + let profile = &state.profiles[0]; + assert!(profile.is_default); + assert!(profile.is_active); + assert_eq!(profile.selected_provider_id, "openai"); + assert_eq!(profile.model.as_deref(), Some("gpt-5.4")); +} + +#[test] +fn profile_state_uses_active_profile_overrides() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +profile = "work" +model = "gpt-5.4" +model_provider = "openai" + +[profiles.work] +model = "openai/gpt-5.4" +model_provider = "openrouter" + +[model_providers.openrouter] +name = "OpenRouter" +base_url = "https://openrouter.ai/api/v1" +wire_api = "responses" +"#, + ) + .unwrap(); + + let state = adapter.load_profile_state().unwrap(); + + assert_eq!(state.active_profile_id, "work"); + let work = state + .profiles + .iter() + .find(|profile| profile.id == "work") + .unwrap(); + assert!(work.is_active); + assert_eq!(work.selected_provider_id, "openrouter"); + assert_eq!(work.model.as_deref(), Some("openai/gpt-5.4")); + let default = state + .profiles + .iter() + .find(|profile| profile.is_default) + .unwrap(); + assert_eq!(default.selected_provider_id, "openai"); + assert_eq!(default.model.as_deref(), Some("gpt-5.4")); +} + +#[test] +fn set_active_profile_preserves_comments() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#"# user note +model = "gpt-5.4" + +[profiles.work] +model_provider = "openrouter" +"#, + ) + .unwrap(); + + let state = adapter.set_active_profile("work").unwrap(); + + assert_eq!(state.active_profile_id, "work"); + let content = fs::read_to_string(adapter.config_path()).unwrap(); + assert!(content.contains("# user note")); + let config = content.parse::().unwrap(); + assert_eq!(config["profile"].as_str(), Some("work")); + + let state = adapter.set_active_profile(DEFAULT_PROFILE_ID).unwrap(); + assert_eq!(state.active_profile_id, DEFAULT_PROFILE_ID); + let config = fs::read_to_string(adapter.config_path()) + .unwrap() + .parse::() + .unwrap(); + assert!(config.get("profile").is_none()); +} + +#[test] +fn set_profile_provider_can_select_openai_without_base_or_key() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +model_provider = "openrouter" + +[profiles.work] +model_provider = "openrouter" + +[model_providers.openrouter] +name = "OpenRouter" +base_url = "https://openrouter.ai/api/v1" +wire_api = "responses" +"#, + ) + .unwrap(); + + let state = adapter.set_profile_provider("work", "openai").unwrap(); + + let work = state + .profiles + .iter() + .find(|profile| profile.id == "work") + .unwrap(); + assert_eq!(work.selected_provider_id, "openai"); + let content = fs::read_to_string(adapter.config_path()).unwrap(); + let config = content.parse::().unwrap(); + assert_eq!( + config["profiles"]["work"]["model_provider"].as_str(), + Some("openai") + ); + + let state = adapter + .set_profile_provider(DEFAULT_PROFILE_ID, "openai") + .unwrap(); + let default = state + .profiles + .iter() + .find(|profile| profile.is_default) + .unwrap(); + assert_eq!(default.selected_provider_id, "openai"); + let config = fs::read_to_string(adapter.config_path()) + .unwrap() + .parse::() + .unwrap(); + assert!(config.get("model_provider").is_none()); +} + #[test] fn add_provider_writes_responses_config_and_inline_token() { let temp = tempfile::tempdir().unwrap(); @@ -167,6 +326,14 @@ experimental_bearer_token = "sk-inline" ); } +#[test] +fn api_key_for_openai_login_provider_is_not_config_backed() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + + assert_eq!(adapter.api_key("openai").unwrap(), None); +} + #[test] fn remove_provider_clears_default_provider() { let temp = tempfile::tempdir().unwrap(); diff --git a/crates/inference/src/lib.rs b/crates/inference/src/lib.rs index 9a6b13d2..a2ca9628 100644 --- a/crates/inference/src/lib.rs +++ b/crates/inference/src/lib.rs @@ -19,7 +19,10 @@ pub use agent::{ AgentProviderModel, AgentProviderSource, AgentProviderState, BuiltInProviderSupport, }; -pub use codex::CodexProviderAdapter; +pub use codex::{ + CodexProfileState, CodexProviderAdapter, CodexProviderState, + DEFAULT_PROFILE_ID as CODEX_DEFAULT_PROFILE_ID, +}; pub use credentials::{CredentialStore, NativeCredentialStore}; pub use error::{InferenceProviderError, Result}; pub use model::{ From 32edab5d36e7cf8abd43db0f207308056e6ebf69 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sun, 26 Apr 2026 16:24:15 +0800 Subject: [PATCH 23/62] fix(desktop): simplify codex provider list --- crates/desktop/src/lib/locales/en.ts | 2 + crates/desktop/src/lib/locales/zh-Hans.ts | 2 + crates/desktop/src/lib/locales/zh-Hant.ts | 2 + .../pages/inference-providers/codex-panel.tsx | 406 +++++++----------- 4 files changed, 167 insertions(+), 245 deletions(-) diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index 5da2a31a..2e1dfd0b 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -136,6 +136,8 @@ export default { "No Aghub OpenAI Responses provider has the same API Base URL and API key. This was likely added directly in Codex.", codexActiveProfile: "Active Profile", codexProfileProvider: "Profile Provider", + codexProfilesUsingProvider: "Profiles", + codexUseForActiveProfile: "Use", codexCurrentRoute: "Current Route", codexActiveProvider: "Active", codexLoginProvider: "Codex login", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index a155235d..1ea6dc26 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -131,6 +131,8 @@ export default { "未在 Aghub OpenAI Responses Provider 中找到相同 API Base URL 和 API key,疑似用户直接在 Codex 里添加。", codexActiveProfile: "当前 Profile", codexProfileProvider: "Profile Provider", + codexProfilesUsingProvider: "使用中的 Profile", + codexUseForActiveProfile: "设为当前", codexCurrentRoute: "当前路由", codexActiveProvider: "当前", codexLoginProvider: "Codex 登录", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index 33a375fa..6d1d8907 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -131,6 +131,8 @@ export default { "未在 Aghub OpenAI Responses Provider 中找到相同 API Base URL 和 API key,疑似使用者直接在 Codex 裡新增。", codexActiveProfile: "目前 Profile", codexProfileProvider: "Profile Provider", + codexProfilesUsingProvider: "使用中的 Profile", + codexUseForActiveProfile: "設為目前", codexCurrentRoute: "目前路由", codexActiveProvider: "目前", codexLoginProvider: "Codex 登入", diff --git a/crates/desktop/src/pages/inference-providers/codex-panel.tsx b/crates/desktop/src/pages/inference-providers/codex-panel.tsx index c68b17ff..b091fda3 100644 --- a/crates/desktop/src/pages/inference-providers/codex-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/codex-panel.tsx @@ -454,13 +454,21 @@ function ProviderMeta({ provider }: { provider: AgentProviderResponse }) { function ProviderActions({ provider, + isActive, isSyncing, + isSelecting, + canSelect, + onSelectForProfile, onEdit, onSync, onDelete, }: { provider: AgentProviderResponse; + isActive: boolean; isSyncing: boolean; + isSelecting: boolean; + canSelect: boolean; + onSelectForProfile: () => void; onEdit: () => void; onSync: () => void; onDelete: () => void; @@ -469,12 +477,18 @@ function ProviderActions({ const matchedProvider = provider.matched_inference_provider; const isBuiltIn = provider.source === "built_in"; - if (isBuiltIn) { - return null; - } - return (
+ {canSelect && !isActive && ( + + )} {matchedProvider && ( @@ -496,39 +510,43 @@ function ProviderActions({ )} - - - - - {t("edit")} - - - - - - {t("delete")} - + {!isBuiltIn && ( + <> + + + + + {t("edit")} + + + + + + {t("delete")} + + + )}
); } @@ -536,18 +554,28 @@ function ProviderActions({ function ProviderRow({ provider, isActive, + profileNames, isSyncing, + isSelecting, + canSelect, + onSelectForProfile, onEdit, onSync, onDelete, }: { provider: AgentProviderResponse; isActive: boolean; + profileNames: string[]; isSyncing: boolean; + isSelecting: boolean; + canSelect: boolean; + onSelectForProfile: () => void; onEdit: () => void; onSync: () => void; onDelete: () => void; }) { + const { t } = useTranslation(); + return (
@@ -558,6 +586,12 @@ function ProviderRow({ />
+ {profileNames.length > 0 && ( + + {t("codexProfilesUsingProvider")}:{" "} + {profileNames.join(", ")} + + )} {provider.api_base_url && ( {provider.api_base_url} @@ -568,7 +602,11 @@ function ProviderRow({ profile.is_active) ?? profiles[0]; - const activeProvider = providers.find( - (provider) => provider.id === activeProfile?.selected_provider_id, - ); - const customProviders = providers.filter( - (provider) => provider.source !== "built_in", - ); - const selectableProviderIds = new Set( - providers.map((provider) => provider.id), + const profileNamesByProviderId = useMemo( + () => + profiles.reduce((map, profile) => { + const names = map.get(profile.selected_provider_id) ?? []; + map.set(profile.selected_provider_id, [...names, profile.name]); + return map; + }, new Map()), + [profiles], ); const profileMutation = useMutation({ @@ -759,231 +797,109 @@ export function CodexInferenceProviderPanel({
- + {isLoading ? (
) : ( <> -
-
- - {t("codexCurrentRoute")} - - {activeProfile ? ( - <> - -
- {activeProvider && ( - - )} - {activeProfile.model && ( - - {t( - "providerModels", - )} - :{" "} - { - activeProfile.model - } - - )} - {activeProvider?.api_base_url && ( - - { - activeProvider.api_base_url - } - - )} -
- - ) : ( -

- {t("codexNoProfiles")} -

- )} -
- -
- { + if (!key) return; + profileMutation.mutate({ + profile_id: String(key), + }); + }} + isDisabled={ + profileMutation.isPending || + profiles.length === 0 + } + variant="secondary" + > + + + + + + + + {profiles.map((profile) => ( + +
+
+ +
+ + ))} + + +
-
- - -
- {customProviders.length === 0 ? ( + {providers.length === 0 ? (

- {t( - "noCodexCustomProviders", - )} + {t("noCodexProviders")}

) : (
- {customProviders.map( - (provider) => ( + {providers.map((provider) => { + const isActive = + activeProfile?.selected_provider_id === + provider.id; + return ( + handleProfileProviderChange( + provider.id, + ) + } onEdit={() => handleEditProvider( provider, @@ -1000,8 +916,8 @@ export function CodexInferenceProviderPanel({ ) } /> - ), - )} + ); + })}
)}
From 95578e887baf45dd826c76e332b7e2edcf6b7853 Mon Sep 17 00:00:00 2001 From: akarachen Date: Sun, 26 Apr 2026 16:27:30 +0800 Subject: [PATCH 24/62] fix(desktop): align codex provider rows --- crates/desktop/src/lib/locales/en.ts | 2 +- crates/desktop/src/lib/locales/zh-Hans.ts | 2 +- crates/desktop/src/lib/locales/zh-Hant.ts | 2 +- .../pages/inference-providers/codex-panel.tsx | 253 ++++++++---------- 4 files changed, 120 insertions(+), 139 deletions(-) diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index 2e1dfd0b..f4c297d1 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -137,7 +137,7 @@ export default { codexActiveProfile: "Active Profile", codexProfileProvider: "Profile Provider", codexProfilesUsingProvider: "Profiles", - codexUseForActiveProfile: "Use", + codexUseForActiveProfile: "Enable", codexCurrentRoute: "Current Route", codexActiveProvider: "Active", codexLoginProvider: "Codex login", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index 1ea6dc26..d1be14ab 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -132,7 +132,7 @@ export default { codexActiveProfile: "当前 Profile", codexProfileProvider: "Profile Provider", codexProfilesUsingProvider: "使用中的 Profile", - codexUseForActiveProfile: "设为当前", + codexUseForActiveProfile: "启用", codexCurrentRoute: "当前路由", codexActiveProvider: "当前", codexLoginProvider: "Codex 登录", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index 6d1d8907..1dba8e19 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -132,7 +132,7 @@ export default { codexActiveProfile: "目前 Profile", codexProfileProvider: "Profile Provider", codexProfilesUsingProvider: "使用中的 Profile", - codexUseForActiveProfile: "設為目前", + codexUseForActiveProfile: "啟用", codexCurrentRoute: "目前路由", codexActiveProvider: "目前", codexLoginProvider: "Codex 登入", diff --git a/crates/desktop/src/pages/inference-providers/codex-panel.tsx b/crates/desktop/src/pages/inference-providers/codex-panel.tsx index b091fda3..2332e79a 100644 --- a/crates/desktop/src/pages/inference-providers/codex-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/codex-panel.tsx @@ -554,7 +554,6 @@ function ProviderActions({ function ProviderRow({ provider, isActive, - profileNames, isSyncing, isSelecting, canSelect, @@ -565,7 +564,6 @@ function ProviderRow({ }: { provider: AgentProviderResponse; isActive: boolean; - profileNames: string[]; isSyncing: boolean; isSelecting: boolean; canSelect: boolean; @@ -574,8 +572,6 @@ function ProviderRow({ onSync: () => void; onDelete: () => void; }) { - const { t } = useTranslation(); - return (
@@ -586,12 +582,6 @@ function ProviderRow({ />
- {profileNames.length > 0 && ( - - {t("codexProfilesUsingProvider")}:{" "} - {profileNames.join(", ")} - - )} {provider.api_base_url && ( {provider.api_base_url} @@ -645,15 +635,6 @@ export function CodexInferenceProviderPanel({ const profiles = codexState?.profiles ?? []; const activeProfile = profiles.find((profile) => profile.is_active) ?? profiles[0]; - const profileNamesByProviderId = useMemo( - () => - profiles.reduce((map, profile) => { - const names = map.get(profile.selected_provider_id) ?? []; - map.set(profile.selected_provider_id, [...names, profile.name]); - return map; - }, new Map()), - [profiles], - ); const profileMutation = useMutation({ ...updateCodexActiveProfileMutationOptions({ @@ -760,6 +741,52 @@ export function CodexInferenceProviderPanel({
+ +
+ ) : ( +
+ {providers.map((provider) => { + const isActive = + activeProfile?.selected_provider_id === + provider.id; + return ( + + handleProfileProviderChange( + provider.id, + ) + } + onEdit={() => + handleEditProvider( + provider, + ) + } + onSync={() => + syncMutation.mutate( + provider.id, + ) + } + onDelete={() => + setDeleteTarget( + provider, + ) + } + /> + ); + })} +
+ )} )} From 3742fa9f7b40692c6171ef4b5f83e677eee9fe34 Mon Sep 17 00:00:00 2001 From: akarachen Date: Tue, 28 Apr 2026 21:47:44 +0800 Subject: [PATCH 25/62] feat: claude/codex/opencode ui for inference provider --- crates/api/src/bin/export-dto.rs | 13 +- crates/api/src/dto/inference.rs | 26 + crates/api/src/lib.rs | 3 + crates/api/src/routes/inference.rs | 51 +- .../dto/ClaudeProviderStateResponse.ts | 7 + .../dto/UpdateClaudeProviderRequest.ts | 7 + crates/desktop/src/generated/dto/index.ts | 2 + crates/desktop/src/lib/api.ts | 17 + crates/desktop/src/lib/locales/en.ts | 19 + crates/desktop/src/lib/locales/zh-Hans.ts | 19 + crates/desktop/src/lib/locales/zh-Hant.ts | 19 + .../desktop/src/pages/inference-providers.tsx | 11 +- .../inference-providers/claude-panel.tsx | 591 ++++++++++++++++++ .../pages/inference-providers/codex-panel.tsx | 104 +-- .../src/requests/inference-providers.ts | 67 ++ crates/inference/src/claude/files.rs | 61 ++ crates/inference/src/claude/mod.rs | 221 +++++++ crates/inference/src/lib.rs | 2 + 18 files changed, 1148 insertions(+), 92 deletions(-) create mode 100644 crates/desktop/src/generated/dto/ClaudeProviderStateResponse.ts create mode 100644 crates/desktop/src/generated/dto/UpdateClaudeProviderRequest.ts create mode 100644 crates/desktop/src/pages/inference-providers/claude-panel.tsx create mode 100644 crates/inference/src/claude/files.rs create mode 100644 crates/inference/src/claude/mod.rs diff --git a/crates/api/src/bin/export-dto.rs b/crates/api/src/bin/export-dto.rs index 5d09ff36..33c1c5c2 100644 --- a/crates/api/src/bin/export-dto.rs +++ b/crates/api/src/bin/export-dto.rs @@ -16,11 +16,12 @@ use aghub_api::dto::{ AgentProviderCredentialDto, AgentProviderMatchedInferenceProviderResponse, AgentProviderModelResponse, AgentProviderResponse, - AgentProviderSourceDto, CodexProfileResponse, - CodexProviderStateResponse, CreateAgentProviderRequest, - CreateInferenceProviderRequest, InferenceProviderFormatDto, - InferenceProviderPasswordResponse, InferenceProviderResponse, - UpdateAgentProviderRequest, UpdateCodexActiveProfileRequest, + AgentProviderSourceDto, ClaudeProviderStateResponse, + CodexProfileResponse, CodexProviderStateResponse, + CreateAgentProviderRequest, CreateInferenceProviderRequest, + InferenceProviderFormatDto, InferenceProviderPasswordResponse, + InferenceProviderResponse, UpdateAgentProviderRequest, + UpdateClaudeProviderRequest, UpdateCodexActiveProfileRequest, UpdateCodexProfileProviderRequest, UpdateInferenceProviderRequest, }, integrations::{ @@ -129,6 +130,8 @@ fn main() -> Result<(), Box> { export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; + export_type::(&cfg)?; + export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; diff --git a/crates/api/src/dto/inference.rs b/crates/api/src/dto/inference.rs index ebb34806..f610a7e4 100644 --- a/crates/api/src/dto/inference.rs +++ b/crates/api/src/dto/inference.rs @@ -340,3 +340,29 @@ pub struct UpdateCodexActiveProfileRequest { pub struct UpdateCodexProfileProviderRequest { pub provider_id: String, } + +#[derive(Debug, Clone, Serialize, TS)] +#[ts(export)] +pub struct ClaudeProviderStateResponse { + pub api_base_url: Option, + pub api_key: Option, + pub model: Option, +} + +impl From for ClaudeProviderStateResponse { + fn from(state: aghub_inference::ClaudeConfigState) -> Self { + Self { + api_base_url: state.api_base_url, + api_key: state.api_key, + model: state.model, + } + } +} + +#[derive(Debug, Deserialize, TS)] +#[ts(export)] +pub struct UpdateClaudeProviderRequest { + pub api_base_url: Option, + pub api_key: Option, + pub model: Option, +} diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index c873111a..86d50da6 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -183,6 +183,9 @@ pub async fn start(options: ApiOptions) -> Result<(), rocket::Error> { routes::inference::get_inference_provider_password, routes::inference::create_inference_provider, routes::inference::update_inference_provider, + routes::inference::get_claude_state, + routes::inference::update_claude_state, + routes::inference::clear_claude_state, routes::inference::delete_inference_provider, routes::skills::open_skill_folder, routes::skills::edit_skill_folder, diff --git a/crates/api/src/routes/inference.rs b/crates/api/src/routes/inference.rs index e594fd65..8658eab7 100644 --- a/crates/api/src/routes/inference.rs +++ b/crates/api/src/routes/inference.rs @@ -1,7 +1,7 @@ use aghub_inference::{ - AgentProviderAdapter, AgentProviderBinding, CodexProviderAdapter, - InferenceProvider, InferenceProviderRepository, InferenceProviderStore, - OpenCodeProviderAdapter, + AgentProviderAdapter, AgentProviderBinding, ClaudeProviderAdapter, + CodexProviderAdapter, InferenceProvider, InferenceProviderRepository, + InferenceProviderStore, OpenCodeProviderAdapter, }; use rocket::http::Status; use rocket::response::status::NoContent; @@ -9,10 +9,11 @@ use rocket::serde::json::Json; use rocket::State; use crate::dto::inference::{ - AgentProviderResponse, CodexProviderStateResponse, - CreateAgentProviderRequest, CreateInferenceProviderRequest, - InferenceProviderPasswordResponse, InferenceProviderResponse, - UpdateAgentProviderRequest, UpdateCodexActiveProfileRequest, + AgentProviderResponse, ClaudeProviderStateResponse, + CodexProviderStateResponse, CreateAgentProviderRequest, + CreateInferenceProviderRequest, InferenceProviderPasswordResponse, + InferenceProviderResponse, UpdateAgentProviderRequest, + UpdateClaudeProviderRequest, UpdateCodexActiveProfileRequest, UpdateCodexProfileProviderRequest, UpdateInferenceProviderRequest, }; use crate::error::{ApiCreated, ApiError, ApiNoContent, ApiResult}; @@ -48,6 +49,10 @@ fn codex_adapter() -> Result { CodexProviderAdapter::global().map_err(ApiError::from) } +fn claude_adapter() -> Result { + ClaudeProviderAdapter::global().map_err(ApiError::from) +} + fn get_inventory_provider( store: &InferenceProviderStore, id: &str, @@ -473,3 +478,35 @@ pub fn delete_inference_provider( store.delete(&provider.id).map_err(ApiError::from)?; Ok(NoContent) } + +// ============================================================================ +// Claude Code routes +// ============================================================================ + +#[get("/inference/agents/claude/state")] +pub fn get_claude_state() -> ApiResult { + let adapter = claude_adapter()?; + let state = adapter.load_config_state().map_err(ApiError::from)?; + Ok(Json(state.into())) +} + +#[put("/inference/agents/claude/state", data = "")] +pub fn update_claude_state( + body: Json, +) -> ApiResult { + let adapter = claude_adapter()?; + let state = aghub_inference::ClaudeConfigState { + api_base_url: body.api_base_url.clone(), + api_key: body.api_key.clone(), + model: body.model.clone(), + }; + adapter.save_config_state(&state).map_err(ApiError::from)?; + Ok(Json(state.into())) +} + +#[delete("/inference/agents/claude/state")] +pub fn clear_claude_state() -> ApiNoContent { + let adapter = claude_adapter()?; + adapter.clear_provider_config().map_err(ApiError::from)?; + Ok(NoContent) +} diff --git a/crates/desktop/src/generated/dto/ClaudeProviderStateResponse.ts b/crates/desktop/src/generated/dto/ClaudeProviderStateResponse.ts new file mode 100644 index 00000000..5a4ef8b1 --- /dev/null +++ b/crates/desktop/src/generated/dto/ClaudeProviderStateResponse.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ClaudeProviderStateResponse = { + api_base_url: string | null; + api_key: string | null; + model: string | null; +}; diff --git a/crates/desktop/src/generated/dto/UpdateClaudeProviderRequest.ts b/crates/desktop/src/generated/dto/UpdateClaudeProviderRequest.ts new file mode 100644 index 00000000..f19149e0 --- /dev/null +++ b/crates/desktop/src/generated/dto/UpdateClaudeProviderRequest.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type UpdateClaudeProviderRequest = { + api_base_url: string | null; + api_key: string | null; + model: string | null; +}; diff --git a/crates/desktop/src/generated/dto/index.ts b/crates/desktop/src/generated/dto/index.ts index 099f40a6..d294385a 100644 --- a/crates/desktop/src/generated/dto/index.ts +++ b/crates/desktop/src/generated/dto/index.ts @@ -6,6 +6,7 @@ export type { AgentProviderModelResponse } from "./AgentProviderModelResponse"; export type { AgentProviderResponse } from "./AgentProviderResponse"; export type { AgentProviderSourceDto } from "./AgentProviderSourceDto"; export type { CapabilitiesDto } from "./CapabilitiesDto"; +export type { ClaudeProviderStateResponse } from "./ClaudeProviderStateResponse"; export type { CodeEditorType } from "./CodeEditorType"; export type { CodexProfileResponse } from "./CodexProfileResponse"; export type { CodexProviderStateResponse } from "./CodexProviderStateResponse"; @@ -66,6 +67,7 @@ export type { ToolPreferencesDto } from "./ToolPreferencesDto"; export type { TransferRequest } from "./TransferRequest"; export type { TransportDto } from "./TransportDto"; export type { UpdateAgentProviderRequest } from "./UpdateAgentProviderRequest"; +export type { UpdateClaudeProviderRequest } from "./UpdateClaudeProviderRequest"; export type { UpdateCodexActiveProfileRequest } from "./UpdateCodexActiveProfileRequest"; export type { UpdateCodexProfileProviderRequest } from "./UpdateCodexProfileProviderRequest"; export type { UpdateInferenceProviderRequest } from "./UpdateInferenceProviderRequest"; diff --git a/crates/desktop/src/lib/api.ts b/crates/desktop/src/lib/api.ts index 6b04cf05..6e6971ea 100644 --- a/crates/desktop/src/lib/api.ts +++ b/crates/desktop/src/lib/api.ts @@ -3,6 +3,7 @@ import type { AgentAvailabilityDto, AgentInfo, AgentProviderResponse, + ClaudeProviderStateResponse, CodeEditorType, CodexProviderStateResponse, CreateAgentProviderRequest, @@ -37,6 +38,7 @@ import type { ToolInfoDto, TransferRequest, UpdateAgentProviderRequest, + UpdateClaudeProviderRequest, UpdateCodexActiveProfileRequest, UpdateCodexProfileProviderRequest, UpdateInferenceProviderRequest, @@ -650,6 +652,21 @@ export function createApi(baseUrl: string) { ) .then(() => undefined); }, + getClaudeState(): Promise { + return client.get("inference/agents/claude/state").json(); + }, + updateClaudeState( + body: UpdateClaudeProviderRequest, + ): Promise { + return client + .put("inference/agents/claude/state", { json: body }) + .json(); + }, + clearClaudeState(): Promise { + return client + .delete("inference/agents/claude/state") + .then(() => undefined); + }, }, }; } diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index f4c297d1..8226a636 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -138,6 +138,7 @@ export default { codexProfileProvider: "Profile Provider", codexProfilesUsingProvider: "Profiles", codexUseForActiveProfile: "Enable", + codexProviderAlreadyActive: "Already the active provider", codexCurrentRoute: "Current Route", codexActiveProvider: "Active", codexLoginProvider: "Codex login", @@ -155,6 +156,24 @@ export default { syncCodexProvider: "Update Codex provider", syncCodexProviderFromInferenceProvider: "Update provider config from {{name}}", + active: "Active", + custom: "Custom", + enable: "Enable", + claudeOfficial: "Claude Official", + claudeOfficialDescription: "Anthropic official API with browser login.", + createClaudeProvider: "Add Claude Code Provider", + noInferenceProvidersForClaude: + "Create an Anthropic inference provider first, then select it here.", + syncClaudeProvider: "Update Claude Code provider", + syncClaudeProviderFromInferenceProvider: "Update config from {{name}}", + claudeProviderUpdated: "Claude Code configuration updated", + claudeProviderUpdateError: "Failed to update Claude Code configuration", + claudeProviderCleared: "Claude Code configuration cleared", + claudeProviderClearError: "Failed to clear Claude Code configuration", + claudeUsingOfficialApi: "Using Anthropic official API with browser login.", + clearClaudeProvider: "Clear Claude Code Configuration", + clearClaudeProviderConfirm: + "Remove all custom API settings and fall back to Anthropic's official API?", agentProviderSourceCustom: "Custom", agentProviderSourceBuiltIn: "Built-in", agentProviderSourceClosedSlot: "Closed slot", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index d1be14ab..1d53a718 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -133,6 +133,7 @@ export default { codexProfileProvider: "Profile Provider", codexProfilesUsingProvider: "使用中的 Profile", codexUseForActiveProfile: "启用", + codexProviderAlreadyActive: "已是当前活跃 provider", codexCurrentRoute: "当前路由", codexActiveProvider: "当前", codexLoginProvider: "Codex 登录", @@ -149,6 +150,24 @@ export default { codexProfileProviderUpdateError: "更新 Codex Profile Provider 失败", syncCodexProvider: "更新 Codex Provider", syncCodexProviderFromInferenceProvider: "从 {{name}} 更新 Provider 配置", + active: "当前", + custom: "自定义", + enable: "启用", + claudeOfficial: "Claude 官方", + claudeOfficialDescription: "Anthropic 官方 API(浏览器登录)。", + createClaudeProvider: "添加 Claude Code Provider", + noInferenceProvidersForClaude: + "请先创建一个 Anthropic 推理 Provider,然后在这里选择。", + syncClaudeProvider: "更新 Claude Code Provider", + syncClaudeProviderFromInferenceProvider: "从 {{name}} 更新配置", + claudeProviderUpdated: "Claude Code 配置已更新", + claudeProviderUpdateError: "更新 Claude Code 配置失败", + claudeProviderCleared: "Claude Code 配置已清除", + claudeProviderClearError: "清除 Claude Code 配置失败", + claudeUsingOfficialApi: "使用 Anthropic 官方 API(浏览器登录)。", + clearClaudeProvider: "清除 Claude Code 配置", + clearClaudeProviderConfirm: + "移除所有自定义 API 设置并回退到 Anthropic 官方 API?", agentProviderSourceCustom: "自定义", agentProviderSourceBuiltIn: "内置", agentProviderSourceClosedSlot: "封闭槽位", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index 1dba8e19..c9482f49 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -133,6 +133,7 @@ export default { codexProfileProvider: "Profile Provider", codexProfilesUsingProvider: "使用中的 Profile", codexUseForActiveProfile: "啟用", + codexProviderAlreadyActive: "已是目前活躍 provider", codexCurrentRoute: "目前路由", codexActiveProvider: "目前", codexLoginProvider: "Codex 登入", @@ -149,6 +150,24 @@ export default { codexProfileProviderUpdateError: "更新 Codex Profile Provider 失敗", syncCodexProvider: "更新 Codex Provider", syncCodexProviderFromInferenceProvider: "從 {{name}} 更新 Provider 配置", + active: "目前", + custom: "自訂", + enable: "啟用", + claudeOfficial: "Claude 官方", + claudeOfficialDescription: "Anthropic 官方 API(瀏覽器登入)。", + createClaudeProvider: "新增 Claude Code Provider", + noInferenceProvidersForClaude: + "請先建立一個 Anthropic 推理 Provider,然後在這裡選擇。", + syncClaudeProvider: "更新 Claude Code Provider", + syncClaudeProviderFromInferenceProvider: "從 {{name}} 更新配置", + claudeProviderUpdated: "Claude Code 配置已更新", + claudeProviderUpdateError: "更新 Claude Code 配置失敗", + claudeProviderCleared: "Claude Code 配置已清除", + claudeProviderClearError: "清除 Claude Code 配置失敗", + claudeUsingOfficialApi: "使用 Anthropic 官方 API(瀏覽器登入)。", + clearClaudeProvider: "清除 Claude Code 配置", + clearClaudeProviderConfirm: + "移除所有自訂 API 設定並回退到 Anthropic 官方 API?", agentProviderSourceCustom: "自訂", agentProviderSourceBuiltIn: "內建", agentProviderSourceClosedSlot: "封閉槽位", diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index ca36fa8f..5caed805 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -42,6 +42,7 @@ import type { import { useApi } from "../hooks/use-api"; import { AgentIcon } from "../lib/agent-icons"; import { cn } from "../lib/utils"; +import { ClaudeInferenceProviderPanel } from "./inference-providers/claude-panel"; import { CodexInferenceProviderPanel } from "./inference-providers/codex-panel"; import { OpenCodeInferenceProviderPanel } from "./inference-providers/opencode-panel"; import { @@ -51,7 +52,7 @@ import { updateInferenceProviderMutationOptions, } from "../requests/inference-providers"; -type CodingAgentId = "opencode" | "codex"; +type CodingAgentId = "opencode" | "codex" | "claude"; type PanelMode = | { type: "detail" } @@ -97,6 +98,10 @@ const CODING_AGENT_OPTIONS: CodingAgentOption[] = [ id: "codex", label: "Codex", }, + { + id: "claude", + label: "Claude Code", + }, ]; interface InferenceProviderFormValues { @@ -1169,6 +1174,10 @@ export default function InferenceProvidersPage() { /> )} + {panel.type === "agent" && panel.agentId === "claude" && ( + + )} + {panel.type === "create" && ( void; +}) { + const { t } = useTranslation(); + + return ( +
+
+
+ + + {isActive && ( + + + {t("active")} + + )} +
+ + {t("claudeOfficialDescription")} + +
+ +
+ {!isActive && ( + + )} +
+
+ ); +} + +function ClaudeProviderRow({ + apiBaseUrl, + apiKey, + model, + matchedProvider, + isActive, + isSyncing, + isDeleting, + onSync, + onDelete, +}: { + apiBaseUrl: string | null; + apiKey: string | null; + model: string | null; + matchedProvider: InferenceProviderResponse | undefined; + isActive: boolean; + isSyncing: boolean; + isDeleting: boolean; + onSync: () => void; + onDelete: () => void; +}) { + const { t } = useTranslation(); + + return ( +
+
+
+ + + {isActive && ( + + + {t("active")} + + )} +
+
+ {apiBaseUrl && ( + {apiBaseUrl} + )} + {apiKey && ( + + + *** + + )} + {model && {model}} +
+
+ +
+ {matchedProvider && ( + + + + + + {t("syncClaudeProviderFromInferenceProvider", { + name: matchedProvider.display_name, + })} + + + )} + + + + + {t("delete")} + +
+
+ ); +} + +export function ClaudeInferenceProviderPanel() { + const { t } = useTranslation(); + const api = useApi(); + const queryClient = useQueryClient(); + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [selectedProviderId, setSelectedProviderId] = useState(""); + + const { + data: state, + isLoading, + isFetching, + refetch, + } = useQuery({ + ...claudeProviderStateQueryOptions({ api }), + }); + + const { data: inventoryProviders = [], isLoading: isInventoryLoading } = + useQuery({ + ...inferenceProviderListQueryOptions({ api }), + }); + + const anthropicProviders = useMemo( + () => + inventoryProviders.filter( + (provider) => provider.format === "anthropic", + ), + [inventoryProviders], + ); + + const { data: matchedProvider } = useQuery({ + queryKey: [ + "claude-provider-match", + state?.api_base_url, + state?.api_key, + ], + queryFn: async () => { + if (!state?.api_base_url || !state?.api_key) { + return undefined; + } + const candidate = inventoryProviders.find( + (p) => p.api_base_url === state.api_base_url, + ); + if (!candidate) return undefined; + try { + const password = await api.inferenceProviders.getPassword( + candidate.name, + ); + return password.api_key === state.api_key + ? candidate + : undefined; + } catch { + return undefined; + } + }, + enabled: Boolean(state?.api_base_url && state?.api_key), + staleTime: 0, + }); + + const updateMutation = useMutation({ + ...updateClaudeProviderMutationOptions({ + api, + queryClient, + onSuccess: async () => { + toast.success(t("claudeProviderUpdated")); + setIsAddDialogOpen(false); + setSelectedProviderId(""); + }, + }), + onError: (error) => { + console.error("Failed to update Claude provider:", error); + toast.danger( + error instanceof Error + ? error.message + : t("claudeProviderUpdateError"), + ); + }, + }); + + const clearMutation = useMutation({ + ...clearClaudeProviderMutationOptions({ + api, + queryClient, + onSuccess: async () => { + setIsDeleteDialogOpen(false); + toast.success(t("claudeProviderCleared")); + }, + }), + onError: (error) => { + console.error("Failed to clear Claude provider:", error); + toast.danger( + error instanceof Error + ? error.message + : t("claudeProviderClearError"), + ); + }, + }); + + const hasCustomConfig = Boolean( + state?.api_base_url || state?.api_key || state?.model, + ); + + const handleAdd = async (event: FormEvent) => { + event.preventDefault(); + if (!selectedProviderId) return; + + const provider = inventoryProviders.find( + (p) => p.id === selectedProviderId, + ); + if (!provider) return; + + try { + const password = await api.inferenceProviders.getPassword( + provider.name, + ); + updateMutation.mutate({ + api_base_url: provider.api_base_url, + api_key: password.api_key, + model: provider.models[0] ?? null, + }); + } catch (error) { + console.error("Failed to get provider password:", error); + toast.danger( + error instanceof Error + ? error.message + : t("inferenceProviderPasswordLoadFailed"), + ); + } + }; + + const handleSync = async () => { + if (!matchedProvider) return; + try { + const password = await api.inferenceProviders.getPassword( + matchedProvider.name, + ); + updateMutation.mutate({ + api_base_url: matchedProvider.api_base_url, + api_key: password.api_key, + model: matchedProvider.models[0] ?? state?.model ?? null, + }); + } catch (error) { + console.error("Failed to sync Claude provider:", error); + toast.danger( + error instanceof Error + ? error.message + : t("claudeProviderUpdateError"), + ); + } + }; + + return ( + <> +
+
+ + +
+ +
+

+ Claude Code +

+
+
+
+ + +
+
+ + + {isLoading ? ( +
+ +
+ ) : ( +
+ + setIsDeleteDialogOpen(true) + } + /> + {hasCustomConfig && ( + + setIsDeleteDialogOpen(true) + } + /> + )} +
+ )} +
+
+
+
+ + { + if (!open) { + setIsAddDialogOpen(false); + setSelectedProviderId(""); + } + }} + > + + + + + + {t("createClaudeProvider")} + + +
+ + {updateMutation.error && ( + + + + + {updateMutation.error instanceof + Error + ? updateMutation.error + .message + : String( + updateMutation.error, + )} + + + + )} + + {isInventoryLoading && ( +
+ +
+ )} + + {!isInventoryLoading && + anthropicProviders.length === 0 && ( + + + + + {t( + "noInferenceProvidersForClaude", + )} + + + + )} + + {!isInventoryLoading && + anthropicProviders.length > 0 && ( + + )} +
+ + + + +
+
+
+
+ + setIsDeleteDialogOpen(false)} + > + + + + + + + {t("clearClaudeProvider")} + + + + {t("clearClaudeProviderConfirm")} + + + + + + + + + + ); +} diff --git a/crates/desktop/src/pages/inference-providers/codex-panel.tsx b/crates/desktop/src/pages/inference-providers/codex-panel.tsx index 2332e79a..0318491a 100644 --- a/crates/desktop/src/pages/inference-providers/codex-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/codex-panel.tsx @@ -1,7 +1,7 @@ import { ArrowPathIcon, CheckCircleIcon, - KeyIcon, + PlayIcon, PencilIcon, PlusIcon, QuestionMarkCircleIcon, @@ -41,7 +41,6 @@ import { deleteCodexProviderMutationOptions, inferenceProviderListQueryOptions, syncCodexProviderMutationOptions, - updateCodexActiveProfileMutationOptions, updateCodexProfileProviderMutationOptions, updateCodexProviderMutationOptions, } from "../../requests/inference-providers"; @@ -371,16 +370,11 @@ function ProviderIdentity({ isActive?: boolean; }) { const { t } = useTranslation(); - const isLoginProvider = provider?.source === "built_in"; const label = provider?.name ?? fallbackId; return (
- {isLoginProvider ? ( - - ) : ( - - )} + {provider && provider.id !== provider.name && ( @@ -390,7 +384,7 @@ function ProviderIdentity({ {isActive && ( - {t("codexActiveProvider")} + {t("active")} )}
@@ -479,16 +473,6 @@ function ProviderActions({ return (
- {canSelect && !isActive && ( - - )} {matchedProvider && ( @@ -547,6 +531,28 @@ function ProviderActions({ )} + + + + + + {isActive + ? t("codexProviderAlreadyActive") + : !canSelect + ? t("codexNoProfiles") + : t("codexUseForActiveProfile")} + +
); } @@ -636,20 +642,6 @@ export function CodexInferenceProviderPanel({ const activeProfile = profiles.find((profile) => profile.is_active) ?? profiles[0]; - const profileMutation = useMutation({ - ...updateCodexActiveProfileMutationOptions({ - api, - queryClient, - }), - onError: (error) => { - console.error("Failed to update Codex profile:", error); - toast.danger( - error instanceof Error - ? error.message - : t("codexProfileUpdateError"), - ); - }, - }); const profileProviderMutation = useMutation({ ...updateCodexProfileProviderMutationOptions({ api, @@ -741,52 +733,6 @@ export function CodexInferenceProviderPanel({
-
- {!isActive && ( - - )} + + + + + + {isActive + ? t("claudeProviderAlreadyActive") + : t("enable")} + +
); @@ -87,6 +102,7 @@ function ClaudeProviderRow({ isActive, isSyncing, isDeleting, + onActivate, onSync, onDelete, }: { @@ -97,6 +113,7 @@ function ClaudeProviderRow({ isActive: boolean; isSyncing: boolean; isDeleting: boolean; + onActivate: () => void; onSync: () => void; onDelete: () => void; }) { @@ -172,6 +189,29 @@ function ClaudeProviderRow({ {t("delete")} + + + + + + {isActive + ? t("claudeProviderAlreadyActive") + : t("enable")} + +
); @@ -398,6 +438,7 @@ export function ClaudeInferenceProviderPanel() { isActive={hasCustomConfig} isSyncing={updateMutation.isPending} isDeleting={clearMutation.isPending} + onActivate={() => {}} onSync={handleSync} onDelete={() => setIsDeleteDialogOpen(true) From dbcb719ded04a989612c195bec3037355c6fa9c5 Mon Sep 17 00:00:00 2001 From: akarachen Date: Tue, 28 Apr 2026 23:36:14 +0800 Subject: [PATCH 27/62] feat: almost work --- crates/api/src/lib.rs | 1 + crates/api/src/routes/inference.rs | 25 +- crates/desktop/src/lib/api.ts | 5 + crates/desktop/src/lib/locales/en.ts | 5 + crates/desktop/src/lib/locales/zh-Hans.ts | 4 + crates/desktop/src/lib/locales/zh-Hant.ts | 4 + .../inference-providers/claude-panel.tsx | 123 ++-- .../pages/inference-providers/codex-panel.tsx | 684 ++++++------------ .../src/requests/inference-providers.ts | 23 + crates/inference/src/claude/mod.rs | 318 ++++++-- crates/inference/src/claude/tests.rs | 230 ++++++ crates/inference/src/codex/files.rs | 35 + crates/inference/src/codex/mapping.rs | 98 ++- crates/inference/src/codex/mod.rs | 119 ++- crates/inference/src/codex/tests.rs | 374 +++++++++- 15 files changed, 1480 insertions(+), 568 deletions(-) create mode 100644 crates/inference/src/claude/tests.rs diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 86d50da6..872e3235 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -186,6 +186,7 @@ pub async fn start(options: ApiOptions) -> Result<(), rocket::Error> { routes::inference::get_claude_state, routes::inference::update_claude_state, routes::inference::clear_claude_state, + routes::inference::clear_codex_state, routes::inference::delete_inference_provider, routes::skills::open_skill_folder, routes::skills::edit_skill_folder, diff --git a/crates/api/src/routes/inference.rs b/crates/api/src/routes/inference.rs index 8658eab7..77bbc82e 100644 --- a/crates/api/src/routes/inference.rs +++ b/crates/api/src/routes/inference.rs @@ -247,11 +247,21 @@ pub fn create_codex_provider( let store = store(state); let (provider, api_key) = get_inventory_provider(&store, &body.inference_provider_id)?; - let binding = codex_adapter()? + let adapter = codex_adapter()?; + let binding = adapter .add_inventory_provider(&provider, &api_key) .map_err(ApiError::from)?; + adapter + .set_active_provider(&binding.id) + .map_err(ApiError::from)?; - Ok((Status::Created, Json(binding.into()))) + Ok(( + Status::Created, + Json( + AgentProviderResponse::from(binding) + .with_matched_inference_provider(&provider), + ), + )) } #[put("/inference/agents/opencode/providers/", data = "")] @@ -498,7 +508,11 @@ pub fn update_claude_state( let state = aghub_inference::ClaudeConfigState { api_base_url: body.api_base_url.clone(), api_key: body.api_key.clone(), + api_key_env_name: None, model: body.model.clone(), + haiku_model: None, + sonnet_model: None, + opus_model: None, }; adapter.save_config_state(&state).map_err(ApiError::from)?; Ok(Json(state.into())) @@ -510,3 +524,10 @@ pub fn clear_claude_state() -> ApiNoContent { adapter.clear_provider_config().map_err(ApiError::from)?; Ok(NoContent) } + +#[delete("/inference/agents/codex/state")] +pub fn clear_codex_state() -> ApiNoContent { + let adapter = codex_adapter()?; + adapter.clear_active_provider().map_err(ApiError::from)?; + Ok(NoContent) +} diff --git a/crates/desktop/src/lib/api.ts b/crates/desktop/src/lib/api.ts index 6e6971ea..dc9876ae 100644 --- a/crates/desktop/src/lib/api.ts +++ b/crates/desktop/src/lib/api.ts @@ -500,6 +500,11 @@ export function createApi(baseUrl: string) { getCodexState(): Promise { return client.get("inference/agents/codex/state").json(); }, + clearCodexState(): Promise { + return client + .delete("inference/agents/codex/state") + .then(() => undefined); + }, getPassword( name: string, ): Promise { diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index 6a74a8ab..eefc3ccd 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -128,6 +128,8 @@ export default { codexProviderUpdated: "Codex provider updated", codexProviderDeleted: "Codex provider deleted", codexProviderDeleteError: "Failed to delete Codex provider", + codexProviderCleared: "Codex provider selection cleared", + codexProviderClearError: "Failed to clear Codex provider selection", codexProviderSynced: "Codex provider updated", codexProviderSyncError: "Failed to update Codex provider", codexBuiltInProvider: "Built-in provider", @@ -155,6 +157,9 @@ export default { syncCodexProvider: "Update Codex provider", syncCodexProviderFromInferenceProvider: "Update provider config from {{name}}", + clearCodexProvider: "Clear Codex Provider Selection", + clearCodexProviderConfirm: + "Switch Codex back to its built-in OpenAI login provider?", active: "Active", custom: "Custom", enable: "Enable", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index 01b350ef..37e7750b 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -123,6 +123,8 @@ export default { codexProviderUpdated: "Codex Provider 已更新", codexProviderDeleted: "Codex Provider 已删除", codexProviderDeleteError: "删除 Codex Provider 失败", + codexProviderCleared: "Codex Provider 选择已清除", + codexProviderClearError: "清除 Codex Provider 选择失败", codexProviderSynced: "Codex Provider 已更新", codexProviderSyncError: "更新 Codex Provider 失败", codexBuiltInProvider: "内置 provider", @@ -149,6 +151,8 @@ export default { codexProfileProviderUpdateError: "更新 Codex Profile Provider 失败", syncCodexProvider: "更新 Codex Provider", syncCodexProviderFromInferenceProvider: "从 {{name}} 更新 Provider 配置", + clearCodexProvider: "清除 Codex Provider 选择", + clearCodexProviderConfirm: "将 Codex 切回内置的 OpenAI 登录 Provider?", active: "当前", custom: "自定义", enable: "启用", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index 420ce67e..21c9c3ec 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -123,6 +123,8 @@ export default { codexProviderUpdated: "Codex Provider 已更新", codexProviderDeleted: "Codex Provider 已刪除", codexProviderDeleteError: "刪除 Codex Provider 失敗", + codexProviderCleared: "Codex Provider 選擇已清除", + codexProviderClearError: "清除 Codex Provider 選擇失敗", codexProviderSynced: "Codex Provider 已更新", codexProviderSyncError: "更新 Codex Provider 失敗", codexBuiltInProvider: "內建 provider", @@ -149,6 +151,8 @@ export default { codexProfileProviderUpdateError: "更新 Codex Profile Provider 失敗", syncCodexProvider: "更新 Codex Provider", syncCodexProviderFromInferenceProvider: "從 {{name}} 更新 Provider 配置", + clearCodexProvider: "清除 Codex Provider 選擇", + clearCodexProviderConfirm: "將 Codex 切回內建的 OpenAI 登入 Provider?", active: "目前", custom: "自訂", enable: "啟用", diff --git a/crates/desktop/src/pages/inference-providers/claude-panel.tsx b/crates/desktop/src/pages/inference-providers/claude-panel.tsx index cd91dce8..dc446755 100644 --- a/crates/desktop/src/pages/inference-providers/claude-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/claude-panel.tsx @@ -5,7 +5,6 @@ import { PlayIcon, PlusIcon, ServerIcon, - TrashIcon, } from "@heroicons/react/24/solid"; import { Alert, @@ -35,6 +34,10 @@ import { updateClaudeProviderMutationOptions, } from "../../requests/inference-providers"; +function sameApiBaseUrl(left: string, right: string) { + return left.trim().replace(/\/+$/, "") === right.trim().replace(/\/+$/, ""); +} + function ClaudeOfficialRow({ isActive, isPending, @@ -95,27 +98,25 @@ function ClaudeOfficialRow({ } function ClaudeProviderRow({ + label, apiBaseUrl, apiKey, model, - matchedProvider, isActive, isSyncing, - isDeleting, onActivate, onSync, - onDelete, + showSync, }: { + label: string; apiBaseUrl: string | null; apiKey: string | null; model: string | null; - matchedProvider: InferenceProviderResponse | undefined; isActive: boolean; isSyncing: boolean; - isDeleting: boolean; onActivate: () => void; onSync: () => void; - onDelete: () => void; + showSync: boolean; }) { const { t } = useTranslation(); @@ -124,12 +125,7 @@ function ClaudeProviderRow({
- + {isActive && ( @@ -152,7 +148,7 @@ function ClaudeProviderRow({
- {matchedProvider && ( + {showSync && ( - - {t("delete")} -
diff --git a/crates/desktop/src/pages/inference-providers/codex-panel.tsx b/crates/desktop/src/pages/inference-providers/codex-panel.tsx index 0318491a..4c98bf0d 100644 --- a/crates/desktop/src/pages/inference-providers/codex-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/codex-panel.tsx @@ -2,9 +2,7 @@ import { ArrowPathIcon, CheckCircleIcon, PlayIcon, - PencilIcon, PlusIcon, - QuestionMarkCircleIcon, ServerIcon, TrashIcon, } from "@heroicons/react/24/solid"; @@ -13,14 +11,11 @@ import { AlertDialog, Button, Card, - FieldError, - Input, Label, ListBox, Modal, Select, Spinner, - TextField, Tooltip, toast, } from "@heroui/react"; @@ -36,19 +31,15 @@ import { useApi } from "../../hooks/use-api"; import { AgentIcon } from "../../lib/agent-icons"; import { cn } from "../../lib/utils"; import { + clearCodexProviderMutationOptions, codexProviderStateQueryOptions, createCodexProviderMutationOptions, deleteCodexProviderMutationOptions, inferenceProviderListQueryOptions, syncCodexProviderMutationOptions, updateCodexProfileProviderMutationOptions, - updateCodexProviderMutationOptions, } from "../../requests/inference-providers"; -type ProviderDialogMode = - | { type: "create" } - | { type: "edit"; provider: AgentProviderResponse }; - function CodexCreateProviderDialog({ isOpen, inventoryProviders, @@ -73,6 +64,7 @@ function CodexCreateProviderDialog({ [inventoryProviders], ); const defaultProviderId = responseProviders[0]?.id ?? ""; + useEffect(() => { if (!isOpen) return; setSelectedProviderId((current) => current || defaultProviderId); @@ -83,7 +75,7 @@ function CodexCreateProviderDialog({ api, queryClient, onSuccess: async () => { - toast.success(t("codexProviderCreated")); + toast.success(t("codexProviderUpdated")); onClose(); }, }), @@ -220,407 +212,198 @@ function CodexCreateProviderDialog({ ); } -function CodexEditProviderDialog({ - isOpen, - provider, - onClose, -}: { - isOpen: boolean; - provider: AgentProviderResponse; - onClose: () => void; -}) { - const { t } = useTranslation(); - const api = useApi(); - const queryClient = useQueryClient(); - const [name, setName] = useState(provider.name); - const [apiKey, setApiKey] = useState(""); - const [nameError, setNameError] = useState(null); - - useEffect(() => { - if (!isOpen) return; - setName(provider.name); - setApiKey(""); - setNameError(null); - }, [isOpen, provider.id, provider.name]); - - const updateMutation = useMutation({ - ...updateCodexProviderMutationOptions({ - api, - queryClient, - onSuccess: async () => { - toast.success(t("codexProviderUpdated")); - onClose(); - }, - }), - }); - - const handleSubmit = (event: FormEvent) => { - event.preventDefault(); - const trimmedName = name.trim(); - if (!trimmedName) { - setNameError(t("validationProviderNameRequired")); - return; - } - - const trimmedApiKey = apiKey.trim(); - updateMutation.mutate({ - id: provider.id, - body: { - name: trimmedName === provider.name ? null : trimmedName, - api_key: trimmedApiKey ? trimmedApiKey : null, - }, - }); - }; - - return ( - { - if (!open) onClose(); - }} - > - - - - - {t("editCodexProvider")} - -
- - {updateMutation.error && ( - - - - - {updateMutation.error instanceof - Error - ? updateMutation.error.message - : String(updateMutation.error)} - - - - )} - - - - { - setName(event.target.value); - if (nameError) setNameError(null); - }} - placeholder={t("providerNamePlaceholder")} - variant="secondary" - /> - {nameError && ( - {nameError} - )} - - - - - - setApiKey(event.target.value) - } - placeholder={t( - "providerApiKeyEditPlaceholder", - )} - variant="secondary" - /> - - - - - - -
-
-
-
- ); -} - -function ProviderIdentity({ - provider, - fallbackId, +function CodexOfficialRow({ isActive, + isPending, + onActivate, }: { - provider: AgentProviderResponse | undefined; - fallbackId: string; - isActive?: boolean; + isActive: boolean; + isPending: boolean; + onActivate: () => void; }) { const { t } = useTranslation(); - const label = provider?.name ?? fallbackId; return ( -
- - - {provider && provider.id !== provider.name && ( - - {provider.id} - - )} - {isActive && ( - - - {t("active")} +
+
+
+ + + {isActive && ( + + + {t("active")} + + )} +
+ + {t("codexLoginProviderInfo")} - )} -
- ); -} - -function ProviderMeta({ provider }: { provider: AgentProviderResponse }) { - const { t } = useTranslation(); - const matchedProvider = provider.matched_inference_provider; +
- if (provider.source === "built_in") { - return ( - - {t("codexLoginProvider")} +
- - - + + - - {t("codexLoginProviderInfo")} + + {isActive + ? t("codexProviderAlreadyActive") + : t("enable")} - - ); - } - - if (matchedProvider) { - return ( - - {t("providerModels")}: {matchedProvider.model_count} - - ); - } - - return ( - - {t("codexConfigProvider")} - - - - - - - - {t("codexConfigProviderInfo")} - - - +
+
); } -function ProviderActions({ +function CodexProviderRow({ provider, + model, isActive, isSyncing, isSelecting, + isDeleting, canSelect, - onSelectForProfile, - onEdit, + onSelect, onSync, onDelete, }: { provider: AgentProviderResponse; + model: string | null; isActive: boolean; isSyncing: boolean; isSelecting: boolean; + isDeleting: boolean; canSelect: boolean; - onSelectForProfile: () => void; - onEdit: () => void; + onSelect: () => void; onSync: () => void; onDelete: () => void; }) { const { t } = useTranslation(); const matchedProvider = provider.matched_inference_provider; - const isBuiltIn = provider.source === "built_in"; + const label = matchedProvider?.display_name ?? provider.name; return ( -
- {matchedProvider && ( +
+
+
+ + + {isActive && ( + + + {t("active")} + + )} +
+
+ + {matchedProvider + ? `${t("providerModels")}: ${ + matchedProvider.model_count + }` + : t("codexConfigProvider")} + + {provider.api_base_url && ( + + {provider.api_base_url} + + )} + {model && {model}} +
+
+ +
+ {matchedProvider && ( + + + + + + {t("syncCodexProviderFromInferenceProvider", { + name: matchedProvider.display_name, + })} + + + )} - {t("syncCodexProviderFromInferenceProvider", { - name: matchedProvider.display_name, - })} + {isActive + ? t("codexProviderAlreadyActive") + : !canSelect + ? t("codexNoProfiles") + : t("enable")} - )} - {!isBuiltIn && ( - <> - - - - - {t("edit")} - - - - - - {t("delete")} - - - )} - - - - - - {isActive - ? t("codexProviderAlreadyActive") - : !canSelect - ? t("codexNoProfiles") - : t("codexUseForActiveProfile")} - - -
- ); -} - -function ProviderRow({ - provider, - isActive, - isSyncing, - isSelecting, - canSelect, - onSelectForProfile, - onEdit, - onSync, - onDelete, -}: { - provider: AgentProviderResponse; - isActive: boolean; - isSyncing: boolean; - isSelecting: boolean; - canSelect: boolean; - onSelectForProfile: () => void; - onEdit: () => void; - onSync: () => void; - onDelete: () => void; -}) { - return ( -
-
- -
- - {provider.api_base_url && ( - - {provider.api_base_url} - - )} -
+ + + + + {t("delete")} +
- -
); } -export function CodexInferenceProviderPanel({ - onEditInferenceProvider, -}: { +export function CodexInferenceProviderPanel(_: { onEditInferenceProvider: (providerName: string) => void; }) { const { t } = useTranslation(); const api = useApi(); const queryClient = useQueryClient(); - const [providerDialog, setProviderDialog] = - useState(null); + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); @@ -637,21 +420,42 @@ export function CodexInferenceProviderPanel({ ...inferenceProviderListQueryOptions({ api }), }); - const providers = codexState?.providers ?? []; - const profiles = codexState?.profiles ?? []; const activeProfile = - profiles.find((profile) => profile.is_active) ?? profiles[0]; + codexState?.profiles.find((profile) => profile.is_active) ?? + codexState?.profiles[0]; + const activeProviderId = activeProfile?.selected_provider_id ?? "openai"; + const isOfficialActive = activeProviderId === "openai"; + const customProviders = (codexState?.providers ?? []).filter( + (provider) => provider.id !== "openai", + ); - const profileProviderMutation = useMutation({ + const clearMutation = useMutation({ + ...clearCodexProviderMutationOptions({ + api, + queryClient, + onSuccess: async () => { + toast.success(t("codexProviderCleared")); + }, + }), + onError: (error) => { + console.error("Failed to clear Codex provider:", error); + toast.danger( + error instanceof Error + ? error.message + : t("codexProviderClearError"), + ); + }, + }); + const selectProviderMutation = useMutation({ ...updateCodexProfileProviderMutationOptions({ api, queryClient, onSuccess: async () => { - toast.success(t("codexProfileProviderUpdated")); + toast.success(t("codexProviderUpdated")); }, }), onError: (error) => { - console.error("Failed to update Codex profile provider:", error); + console.error("Failed to switch Codex provider:", error); toast.danger( error instanceof Error ? error.message @@ -695,24 +499,6 @@ export function CodexInferenceProviderPanel({ }, }); - const handleEditProvider = (provider: AgentProviderResponse) => { - const matchedProvider = provider.matched_inference_provider; - if (matchedProvider) { - onEditInferenceProvider(matchedProvider.name); - return; - } - - setProviderDialog({ type: "edit", provider }); - }; - - const handleProfileProviderChange = (providerId: string) => { - if (!activeProfile) return; - profileProviderMutation.mutate({ - profileId: activeProfile.id, - body: { provider_id: providerId }, - }); - }; - return ( <>
@@ -760,9 +546,7 @@ export function CodexInferenceProviderPanel({
) : ( - <> - {providers.length === 0 ? ( +
+ + clearMutation.mutate() + } + /> + {customProviders.length === 0 ? (

{t("noCodexProviders")} @@ -788,9 +579,7 @@ export function CodexInferenceProviderPanel({ "createCodexProvider", )} onPress={() => - setProviderDialog({ - type: "create", - }) + setIsAddDialogOpen(true) } > @@ -798,57 +587,66 @@ export function CodexInferenceProviderPanel({

) : ( -
- {providers.map((provider) => { - const isActive = - activeProfile?.selected_provider_id === - provider.id; - return ( - - handleProfileProviderChange( - provider.id, - ) - } - onEdit={() => - handleEditProvider( - provider, - ) - } - onSync={() => - syncMutation.mutate( - provider.id, - ) - } - onDelete={() => - setDeleteTarget( - provider, - ) - } - /> - ); - })} -
+ customProviders.map((provider) => ( + { + if (!activeProfile) return; + selectProviderMutation.mutate( + { + profileId: + activeProfile.id, + body: { + provider_id: + provider.id, + }, + }, + ); + }} + onSync={() => + syncMutation.mutate( + provider.id, + ) + } + onDelete={() => + setDeleteTarget(provider) + } + /> + )) )} - +
)} @@ -856,18 +654,11 @@ export function CodexInferenceProviderPanel({
setProviderDialog(null)} + onClose={() => setIsAddDialogOpen(false)} /> - {providerDialog?.type === "edit" && ( - setProviderDialog(null)} - /> - )} { - if (deleteTarget) { - deleteMutation.mutate(deleteTarget.id); - } + if (!deleteTarget) return; + deleteMutation.mutate(deleteTarget.id); }} > {t("delete")} diff --git a/crates/desktop/src/requests/inference-providers.ts b/crates/desktop/src/requests/inference-providers.ts index 9270c4e5..a25a4274 100644 --- a/crates/desktop/src/requests/inference-providers.ts +++ b/crates/desktop/src/requests/inference-providers.ts @@ -96,6 +96,9 @@ export async function invalidateCodexProviderQueries(queryClient: QueryClient) { await queryClient.invalidateQueries({ queryKey: queryKeys.inferenceProviders.agent("codex"), }); + await queryClient.invalidateQueries({ + queryKey: queryKeys.inferenceProviders.agentState("codex"), + }); } interface CreateInferenceProviderMutationParams { @@ -442,6 +445,26 @@ export function deleteCodexProviderMutationOptions({ }); } +interface ClearCodexProviderMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: () => void | Promise; +} + +export function clearCodexProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: ClearCodexProviderMutationParams) { + return mutationOptions({ + mutationFn: () => api.inferenceProviders.clearCodexState(), + onSuccess: async () => { + await invalidateCodexProviderQueries(queryClient); + await onSuccess?.(); + }, + }); +} + interface DeleteInferenceModelVariables { providerName: string; modelName: string; diff --git a/crates/inference/src/claude/mod.rs b/crates/inference/src/claude/mod.rs index fd460cce..abd16c5a 100644 --- a/crates/inference/src/claude/mod.rs +++ b/crates/inference/src/claude/mod.rs @@ -5,9 +5,13 @@ mod files; +#[cfg(test)] +mod tests; + use std::path::{Path, PathBuf}; use serde_json::json; +use serde_json::{Map, Value}; use crate::agent::{ AgentCredentialSupport, AgentModelSelection, AgentProviderAdapter, @@ -19,6 +23,20 @@ use crate::error::Result; pub(super) const AGENT_ID: &str = "claude"; pub(super) const PRIMARY_PROVIDER_ID: &str = "primary"; +const API_BASE_URL_ENV: &str = "ANTHROPIC_BASE_URL"; +const API_KEY_ENV: &str = "ANTHROPIC_API_KEY"; +const AUTH_TOKEN_ENV: &str = "ANTHROPIC_AUTH_TOKEN"; +const MODEL_ENV: &str = "ANTHROPIC_MODEL"; +const LEGACY_SMALL_FAST_MODEL_ENV: &str = "ANTHROPIC_SMALL_FAST_MODEL"; +const LEGACY_REASONING_MODEL_ENV: &str = "ANTHROPIC_REASONING_MODEL"; +const DEFAULT_HAIKU_MODEL_ENV: &str = "ANTHROPIC_DEFAULT_HAIKU_MODEL"; +const DEFAULT_SONNET_MODEL_ENV: &str = "ANTHROPIC_DEFAULT_SONNET_MODEL"; +const DEFAULT_OPUS_MODEL_ENV: &str = "ANTHROPIC_DEFAULT_OPUS_MODEL"; +const SETTINGS_MODEL_KEY: &str = "model"; +const LEGACY_API_BASE_URL_KEY: &str = "apiBaseUrl"; +const LEGACY_PRIMARY_MODEL_KEY: &str = "primaryModel"; +const LEGACY_SMALL_FAST_MODEL_KEY: &str = "smallFastModel"; + /// Provider adapter for Claude Code. #[derive(Debug, Clone)] pub struct ClaudeProviderAdapter { @@ -30,10 +48,18 @@ pub struct ClaudeProviderAdapter { pub struct ClaudeConfigState { /// API base URL from `ANTHROPIC_BASE_URL`. pub api_base_url: Option, - /// API key from `ANTHROPIC_API_KEY`. + /// Effective API key from `ANTHROPIC_AUTH_TOKEN` or `ANTHROPIC_API_KEY`. pub api_key: Option, - /// Model from `ANTHROPIC_MODEL`. + /// Credential field currently used inside `env`. + pub api_key_env_name: Option, + /// Effective primary model from top-level `model` or `ANTHROPIC_MODEL`. pub model: Option, + /// Default Haiku alias model. + pub haiku_model: Option, + /// Default Sonnet alias model. + pub sonnet_model: Option, + /// Default Opus alias model. + pub opus_model: Option, } impl ClaudeProviderAdapter { @@ -57,33 +83,22 @@ impl ClaudeProviderAdapter { /// Load raw Claude configuration state from `settings.json`. pub fn load_config_state(&self) -> Result { let config = files::read_config(&self.config_path)?; - let env = config - .get("env") - .and_then(|v| v.as_object()) - .cloned() - .unwrap_or_default(); - - Ok(ClaudeConfigState { - api_base_url: env - .get("ANTHROPIC_BASE_URL") - .and_then(|v| v.as_str()) - .map(ToString::to_string), - api_key: env - .get("ANTHROPIC_API_KEY") - .and_then(|v| v.as_str()) - .map(ToString::to_string), - model: env - .get("ANTHROPIC_MODEL") - .and_then(|v| v.as_str()) - .map(ToString::to_string), - }) + Ok(config_state_from_value(&config)) } /// Save Claude configuration state to `settings.json`. /// - /// Only mutates the `env` object; preserves all other settings. + /// Mutates provider-related keys while preserving unrelated settings. pub fn save_config_state(&self, state: &ClaudeConfigState) -> Result<()> { let mut config = files::read_config(&self.config_path)?; + let current = config_state_from_value(&config); + let resolved = resolve_state(state, ¤t); + + if let Some(model) = &resolved.model { + config[SETTINGS_MODEL_KEY] = json!(model); + } else if let Some(obj) = config.as_object_mut() { + obj.remove(SETTINGS_MODEL_KEY); + } if config.get("env").is_none() { config["env"] = json!({}); @@ -91,23 +106,41 @@ impl ClaudeProviderAdapter { if let Some(env) = config.get_mut("env").and_then(|v| v.as_object_mut()) { - if let Some(url) = &state.api_base_url { - env.insert("ANTHROPIC_BASE_URL".to_string(), json!(url)); + if let Some(url) = &resolved.api_base_url { + env.insert(API_BASE_URL_ENV.to_string(), json!(url)); } else { - env.remove("ANTHROPIC_BASE_URL"); + env.remove(API_BASE_URL_ENV); } - if let Some(key) = &state.api_key { - env.insert("ANTHROPIC_API_KEY".to_string(), json!(key)); - } else { - env.remove("ANTHROPIC_API_KEY"); + env.remove(API_KEY_ENV); + env.remove(AUTH_TOKEN_ENV); + if let Some(key) = &resolved.api_key { + let key_name = resolved + .api_key_env_name + .as_deref() + .and_then(normalize_api_key_env_name) + .unwrap_or(AUTH_TOKEN_ENV); + env.insert(key_name.to_string(), json!(key)); } - if let Some(model) = &state.model { - env.insert("ANTHROPIC_MODEL".to_string(), json!(model)); - } else { - env.remove("ANTHROPIC_MODEL"); - } + write_env_string(env, MODEL_ENV, resolved.model.as_deref()); + write_env_string( + env, + DEFAULT_HAIKU_MODEL_ENV, + resolved.haiku_model.as_deref(), + ); + write_env_string( + env, + DEFAULT_SONNET_MODEL_ENV, + resolved.sonnet_model.as_deref(), + ); + write_env_string( + env, + DEFAULT_OPUS_MODEL_ENV, + resolved.opus_model.as_deref(), + ); + env.remove(LEGACY_SMALL_FAST_MODEL_ENV); + env.remove(LEGACY_REASONING_MODEL_ENV); if env.is_empty() { if let Some(obj) = config.as_object_mut() { @@ -122,21 +155,22 @@ impl ClaudeProviderAdapter { /// Clear all provider-related env overrides. /// - /// Removes `ANTHROPIC_BASE_URL`, `ANTHROPIC_API_KEY`, and `ANTHROPIC_MODEL` - /// from the `env` object, falling back to Claude Code's default behavior - /// (using Anthropic's official API with browser login). + /// Removes provider-related env and model overrides, falling back to + /// Claude Code's default behavior. pub fn clear_provider_config(&self) -> Result<()> { let mut config = files::read_config(&self.config_path)?; if let Some(env) = config.get_mut("env").and_then(|v| v.as_object_mut()) { - env.remove("ANTHROPIC_BASE_URL"); - env.remove("ANTHROPIC_API_KEY"); - env.remove("ANTHROPIC_MODEL"); - env.remove("ANTHROPIC_AUTH_TOKEN"); - env.remove("ANTHROPIC_DEFAULT_HAIKU_MODEL"); - env.remove("ANTHROPIC_DEFAULT_SONNET_MODEL"); - env.remove("ANTHROPIC_DEFAULT_OPUS_MODEL"); + env.remove(API_BASE_URL_ENV); + env.remove(API_KEY_ENV); + env.remove(AUTH_TOKEN_ENV); + env.remove(MODEL_ENV); + env.remove(LEGACY_SMALL_FAST_MODEL_ENV); + env.remove(LEGACY_REASONING_MODEL_ENV); + env.remove(DEFAULT_HAIKU_MODEL_ENV); + env.remove(DEFAULT_SONNET_MODEL_ENV); + env.remove(DEFAULT_OPUS_MODEL_ENV); if env.is_empty() { if let Some(obj) = config.as_object_mut() { @@ -145,6 +179,13 @@ impl ClaudeProviderAdapter { } } + if let Some(obj) = config.as_object_mut() { + obj.remove(SETTINGS_MODEL_KEY); + obj.remove(LEGACY_API_BASE_URL_KEY); + obj.remove(LEGACY_PRIMARY_MODEL_KEY); + obj.remove(LEGACY_SMALL_FAST_MODEL_KEY); + } + files::write_config(&self.config_path, &config)?; Ok(()) } @@ -173,19 +214,18 @@ impl AgentProviderAdapter for ClaudeProviderAdapter { source_provider_id: None, name: "Custom".to_string(), format: Some(crate::model::InferenceProviderFormat::Anthropic), - api_base_url: state.api_base_url, + api_base_url: state.api_base_url.clone(), credential: state .api_key + .as_ref() .map(|_| AgentProviderCredential::EnvVar { - name: "ANTHROPIC_API_KEY".to_string(), + name: state + .api_key_env_name + .clone() + .unwrap_or_else(|| AUTH_TOKEN_ENV.to_string()), }) .unwrap_or(AgentProviderCredential::None), - models: state - .model - .clone() - .into_iter() - .map(crate::agent::AgentProviderModel::new) - .collect(), + models: provider_models_from_state(&state), source: AgentProviderSource::ClosedSlot, }) } else { @@ -202,20 +242,186 @@ impl AgentProviderAdapter for ClaudeProviderAdapter { fn save_providers(&self, state: &AgentProviderState) -> Result<()> { state.validate(AGENT_ID, &self.capabilities())?; + let current = self.load_config_state()?; let provider = state.providers.first(); let config_state = ClaudeConfigState { api_base_url: provider.and_then(|p| p.api_base_url.clone()), api_key: match provider.map(|p| &p.credential) { Some(AgentProviderCredential::EnvVar { .. }) => { - // We don't store the actual key in the adapter; - // the caller should use save_config_state directly - None + // Preserve the existing key when generic state save + // round-trips an env-backed Claude provider. + current.api_key + } + _ => None, + }, + api_key_env_name: match provider.map(|p| &p.credential) { + Some(AgentProviderCredential::EnvVar { name }) => { + Some(name.clone()) } _ => None, }, model: state.default_model.as_ref().map(|m| m.model_id.clone()), + haiku_model: None, + sonnet_model: None, + opus_model: None, }; self.save_config_state(&config_state) } } + +fn config_state_from_value(config: &Value) -> ClaudeConfigState { + let env = config + .get("env") + .and_then(Value::as_object) + .cloned() + .unwrap_or_default(); + let model = string_field(config, SETTINGS_MODEL_KEY) + .or_else(|| env_string(&env, MODEL_ENV)); + let legacy_small_fast = env_string(&env, LEGACY_SMALL_FAST_MODEL_ENV); + + ClaudeConfigState { + api_base_url: env_string(&env, API_BASE_URL_ENV), + api_key: active_api_key(&env), + api_key_env_name: active_api_key_env_name(&env), + model: model.clone(), + haiku_model: env_string(&env, DEFAULT_HAIKU_MODEL_ENV) + .or_else(|| legacy_small_fast.clone()) + .or_else(|| model.clone()), + sonnet_model: env_string(&env, DEFAULT_SONNET_MODEL_ENV) + .or_else(|| model.clone()) + .or_else(|| legacy_small_fast.clone()), + opus_model: env_string(&env, DEFAULT_OPUS_MODEL_ENV) + .or_else(|| model.clone()) + .or(legacy_small_fast), + } +} + +fn active_api_key(env: &Map) -> Option { + active_api_key_env_name(env).and_then(|name| env_string(env, &name)) +} + +fn active_api_key_env_name(env: &Map) -> Option { + if env_string(env, AUTH_TOKEN_ENV).is_some() { + return Some(AUTH_TOKEN_ENV.to_string()); + } + if env_string(env, API_KEY_ENV).is_some() { + return Some(API_KEY_ENV.to_string()); + } + None +} + +fn resolve_state( + state: &ClaudeConfigState, + current: &ClaudeConfigState, +) -> ClaudeConfigState { + let model_changed = state.model != current.model; + let model = state.model.clone(); + + ClaudeConfigState { + api_base_url: state.api_base_url.clone(), + api_key: state.api_key.clone(), + api_key_env_name: match &state.api_key { + Some(_) => state + .api_key_env_name + .as_deref() + .and_then(normalize_api_key_env_name) + .map(ToString::to_string) + .or_else(|| current.api_key_env_name.clone()) + .or_else(|| Some(AUTH_TOKEN_ENV.to_string())), + None => None, + }, + model: model.clone(), + haiku_model: resolve_model_alias( + &state.haiku_model, + ¤t.haiku_model, + &model, + model_changed, + ), + sonnet_model: resolve_model_alias( + &state.sonnet_model, + ¤t.sonnet_model, + &model, + model_changed, + ), + opus_model: resolve_model_alias( + &state.opus_model, + ¤t.opus_model, + &model, + model_changed, + ), + } +} + +fn resolve_model_alias( + explicit: &Option, + current: &Option, + model: &Option, + model_changed: bool, +) -> Option { + if model.is_none() { + return None; + } + + explicit + .clone() + .or_else(|| (!model_changed).then(|| current.clone()).flatten()) + .or_else(|| model.clone()) +} + +fn normalize_api_key_env_name(name: &str) -> Option<&'static str> { + match name { + AUTH_TOKEN_ENV => Some(AUTH_TOKEN_ENV), + API_KEY_ENV => Some(API_KEY_ENV), + _ => None, + } +} + +fn write_env_string( + env: &mut Map, + key: &str, + value: Option<&str>, +) { + if let Some(value) = value { + env.insert(key.to_string(), json!(value)); + } else { + env.remove(key); + } +} + +fn env_string(env: &Map, key: &str) -> Option { + env.get(key) + .and_then(Value::as_str) + .map(ToString::to_string) +} + +fn string_field(value: &Value, key: &str) -> Option { + value + .get(key) + .and_then(Value::as_str) + .map(ToString::to_string) +} + +fn provider_models_from_state( + state: &ClaudeConfigState, +) -> Vec { + let mut models = Vec::new(); + push_unique_model(&mut models, state.model.as_deref()); + push_unique_model(&mut models, state.haiku_model.as_deref()); + push_unique_model(&mut models, state.sonnet_model.as_deref()); + push_unique_model(&mut models, state.opus_model.as_deref()); + models +} + +fn push_unique_model( + models: &mut Vec, + model_id: Option<&str>, +) { + let Some(model_id) = model_id else { + return; + }; + if models.iter().any(|model| model.id == model_id) { + return; + } + models.push(crate::agent::AgentProviderModel::new(model_id)); +} diff --git a/crates/inference/src/claude/tests.rs b/crates/inference/src/claude/tests.rs new file mode 100644 index 00000000..36032557 --- /dev/null +++ b/crates/inference/src/claude/tests.rs @@ -0,0 +1,230 @@ +use std::fs; + +use serde_json::Value; + +use super::*; +use crate::agent::{ + AgentProviderAdapter, AgentProviderCredential, AgentProviderSource, +}; +use crate::model::InferenceProviderFormat; + +fn adapter(temp: &tempfile::TempDir) -> ClaudeProviderAdapter { + ClaudeProviderAdapter::new(temp.path().join("settings.json")) +} + +#[test] +fn load_reads_auth_token_and_normalized_model_state() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#"{ + "model": "claude-top", + "permissions": { "allow": ["Read"] }, + "env": { + "ANTHROPIC_BASE_URL": "https://api.example.com", + "ANTHROPIC_AUTH_TOKEN": "sk-test", + "ANTHROPIC_MODEL": "claude-env", + "ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet", + "KEEP": "1" + } + }"#, + ) + .unwrap(); + + let config = adapter.load_config_state().unwrap(); + assert_eq!( + config.api_base_url.as_deref(), + Some("https://api.example.com") + ); + assert_eq!(config.api_key.as_deref(), Some("sk-test")); + assert_eq!( + config.api_key_env_name.as_deref(), + Some("ANTHROPIC_AUTH_TOKEN") + ); + assert_eq!(config.model.as_deref(), Some("claude-top")); + assert_eq!(config.haiku_model.as_deref(), Some("claude-haiku")); + assert_eq!(config.sonnet_model.as_deref(), Some("claude-sonnet")); + assert_eq!(config.opus_model.as_deref(), Some("claude-top")); + + let state = adapter.load_providers().unwrap(); + assert_eq!(state.providers.len(), 1); + assert_eq!(state.default_model.unwrap().model_id, "claude-top"); + + let provider = &state.providers[0]; + assert_eq!(provider.id, PRIMARY_PROVIDER_ID); + assert_eq!(provider.name, "Custom"); + assert_eq!(provider.source, AgentProviderSource::ClosedSlot); + assert_eq!(provider.format, Some(InferenceProviderFormat::Anthropic)); + assert_eq!( + provider.api_base_url.as_deref(), + Some("https://api.example.com") + ); + assert_eq!( + provider.credential, + AgentProviderCredential::EnvVar { + name: "ANTHROPIC_AUTH_TOKEN".to_string() + } + ); + assert_eq!(provider.models.len(), 3); + assert_eq!(provider.models[0].id, "claude-top"); + assert_eq!(provider.models[1].id, "claude-haiku"); + assert_eq!(provider.models[2].id, "claude-sonnet"); +} + +#[test] +fn save_providers_preserves_existing_auth_field_and_alias_models() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#"{ + "model": "claude-sonnet-4-5", + "env": { + "ANTHROPIC_BASE_URL": "https://api.example.com", + "ANTHROPIC_AUTH_TOKEN": "sk-test", + "ANTHROPIC_MODEL": "claude-sonnet-4-5", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-5", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-5", + "KEEP": "1" + } + }"#, + ) + .unwrap(); + + let state = adapter.load_providers().unwrap(); + adapter.save_providers(&state).unwrap(); + + let config: Value = serde_json::from_str( + &fs::read_to_string(adapter.config_path()).unwrap(), + ) + .unwrap(); + assert_eq!( + config["env"]["ANTHROPIC_AUTH_TOKEN"].as_str(), + Some("sk-test") + ); + assert!(config["env"].get("ANTHROPIC_API_KEY").is_none()); + assert_eq!( + config["env"]["ANTHROPIC_BASE_URL"].as_str(), + Some("https://api.example.com") + ); + assert_eq!(config["model"].as_str(), Some("claude-sonnet-4-5")); + assert_eq!( + config["env"]["ANTHROPIC_MODEL"].as_str(), + Some("claude-sonnet-4-5") + ); + assert_eq!( + config["env"]["ANTHROPIC_DEFAULT_HAIKU_MODEL"].as_str(), + Some("claude-haiku-4-5") + ); + assert_eq!( + config["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"].as_str(), + Some("claude-sonnet-4-5") + ); + assert_eq!( + config["env"]["ANTHROPIC_DEFAULT_OPUS_MODEL"].as_str(), + Some("claude-opus-4-5") + ); + assert_eq!(config["env"]["KEEP"].as_str(), Some("1")); +} + +#[test] +fn save_config_state_model_change_defaults_aliases_and_cleans_legacy_fields() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#"{ + "env": { + "ANTHROPIC_BASE_URL": "https://api.example.com", + "ANTHROPIC_API_KEY": "sk-old", + "ANTHROPIC_MODEL": "claude-old", + "ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-old" + } + }"#, + ) + .unwrap(); + + adapter + .save_config_state(&ClaudeConfigState { + api_base_url: Some("https://api.example.com".to_string()), + api_key: Some("sk-new".to_string()), + api_key_env_name: None, + model: Some("claude-new".to_string()), + haiku_model: None, + sonnet_model: None, + opus_model: None, + }) + .unwrap(); + + let config: Value = serde_json::from_str( + &fs::read_to_string(adapter.config_path()).unwrap(), + ) + .unwrap(); + assert_eq!(config["model"].as_str(), Some("claude-new")); + assert_eq!(config["env"]["ANTHROPIC_API_KEY"].as_str(), Some("sk-new")); + assert!(config["env"].get("ANTHROPIC_AUTH_TOKEN").is_none()); + assert_eq!( + config["env"]["ANTHROPIC_MODEL"].as_str(), + Some("claude-new") + ); + assert_eq!( + config["env"]["ANTHROPIC_DEFAULT_HAIKU_MODEL"].as_str(), + Some("claude-new") + ); + assert_eq!( + config["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"].as_str(), + Some("claude-new") + ); + assert_eq!( + config["env"]["ANTHROPIC_DEFAULT_OPUS_MODEL"].as_str(), + Some("claude-new") + ); + assert!(config["env"].get("ANTHROPIC_SMALL_FAST_MODEL").is_none()); +} + +#[test] +fn clear_provider_config_removes_only_provider_env_keys() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#"{ + "model": "claude-sonnet-4-5", + "permissions": { "allow": ["Read"] }, + "env": { + "ANTHROPIC_BASE_URL": "https://api.example.com", + "ANTHROPIC_API_KEY": "sk-test", + "ANTHROPIC_MODEL": "claude-sonnet-4-5", + "ANTHROPIC_AUTH_TOKEN": "legacy", + "ANTHROPIC_SMALL_FAST_MODEL": "small-fast", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "haiku", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "sonnet", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "opus", + "KEEP": "1" + } + }"#, + ) + .unwrap(); + + adapter.clear_provider_config().unwrap(); + + let config: Value = serde_json::from_str( + &fs::read_to_string(adapter.config_path()).unwrap(), + ) + .unwrap(); + let env = config["env"].as_object().unwrap(); + assert_eq!(env.get("KEEP").and_then(Value::as_str), Some("1")); + assert!(!env.contains_key("ANTHROPIC_BASE_URL")); + assert!(!env.contains_key("ANTHROPIC_API_KEY")); + assert!(!env.contains_key("ANTHROPIC_MODEL")); + assert!(!env.contains_key("ANTHROPIC_AUTH_TOKEN")); + assert!(!env.contains_key("ANTHROPIC_SMALL_FAST_MODEL")); + assert!(!env.contains_key("ANTHROPIC_DEFAULT_HAIKU_MODEL")); + assert!(!env.contains_key("ANTHROPIC_DEFAULT_SONNET_MODEL")); + assert!(!env.contains_key("ANTHROPIC_DEFAULT_OPUS_MODEL")); + assert!(config.get("permissions").is_some()); + assert!(config.get("model").is_none()); +} diff --git a/crates/inference/src/codex/files.rs b/crates/inference/src/codex/files.rs index aa1dc6e2..b9c80031 100644 --- a/crates/inference/src/codex/files.rs +++ b/crates/inference/src/codex/files.rs @@ -3,6 +3,7 @@ use std::fs; use std::path::{Path, PathBuf}; +use serde_json::{json, Value}; use toml_edit::DocumentMut; use super::AGENT_ID; @@ -30,6 +31,29 @@ pub(super) fn write_config(path: &Path, config: &DocumentMut) -> Result<()> { Ok(()) } +pub(super) fn read_auth(path: &Path) -> Result { + let content = match fs::read_to_string(path) { + Ok(content) => content, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return Ok(json!({})); + } + Err(error) => return Err(error.into()), + }; + + serde_json::from_str(&content) + .map_err(|error| invalid_credential_store(path, error.to_string())) +} + +pub(super) fn write_auth(path: &Path, auth: &Value) -> Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let json = serde_json::to_string_pretty(auth) + .map_err(|error| invalid_credential_store(path, error.to_string()))?; + fs::write(path, json)?; + Ok(()) +} + pub(super) fn default_global_config_path() -> Result { let codex_home = std::env::var_os("CODEX_HOME") .map(PathBuf::from) @@ -55,3 +79,14 @@ pub(super) fn invalid_config( message: message.into(), } } + +fn invalid_credential_store( + path: &Path, + message: impl Into, +) -> InferenceProviderError { + InferenceProviderError::InvalidAgentCredentialStore { + agent_id: AGENT_ID.to_string(), + path: path.display().to_string(), + message: message.into(), + } +} diff --git a/crates/inference/src/codex/mapping.rs b/crates/inference/src/codex/mapping.rs index d8a31dd0..133170ba 100644 --- a/crates/inference/src/codex/mapping.rs +++ b/crates/inference/src/codex/mapping.rs @@ -1,5 +1,6 @@ //! Mapping between Codex config.toml and normalized agent state. +use serde_json::Value as JsonValue; use toml_edit::{value, Item, Table}; use crate::agent::{ @@ -16,13 +17,15 @@ const AUTHORIZATION_HEADER: &str = "Authorization"; const RESERVED_PROVIDER_IDS: &[&str] = &[OPENAI_PROVIDER_ID, "ollama", "lmstudio"]; -pub(super) fn built_in_openai_binding() -> AgentProviderBinding { +pub(super) fn built_in_openai_binding( + api_base_url: Option, +) -> AgentProviderBinding { AgentProviderBinding { id: OPENAI_PROVIDER_ID.to_string(), source_provider_id: None, name: "OpenAI".to_string(), format: Some(InferenceProviderFormat::OpenAiResponses), - api_base_url: None, + api_base_url, credential: AgentProviderCredential::AgentStore { id: Some(OPENAI_PROVIDER_ID.to_string()), }, @@ -35,6 +38,7 @@ pub(super) fn binding_from_table( provider_id: &str, table: &Table, ) -> Result { + let credential = credential_from_table(table); Ok(AgentProviderBinding { id: clean_provider_id(provider_id)?, source_provider_id: None, @@ -42,7 +46,14 @@ pub(super) fn binding_from_table( .unwrap_or_else(|| provider_id.to_string()), format: format_from_table(table), api_base_url: string_field(table, "base_url"), - credential: credential_from_table(table), + credential: match credential { + AgentProviderCredential::AgentStore { .. } => { + AgentProviderCredential::AgentStore { + id: Some(provider_id.to_string()), + } + } + other => other, + }, models: Vec::::new(), source: AgentProviderSource::Custom, }) @@ -59,12 +70,7 @@ pub(super) fn provider_table_from_binding( table["base_url"] = value(api_base_url.clone()); } table["wire_api"] = value(WIRE_API_RESPONSES); - table.remove("env_key"); - table.remove("requires_openai_auth"); - table.remove("auth"); - if let Some(api_key) = api_key { - table["experimental_bearer_token"] = value(api_key.to_string()); - } + apply_credential(&mut table, &binding.credential, api_key); table } @@ -88,6 +94,14 @@ pub(super) fn api_key_from_table(table: &Table) -> Option { .map(ToString::to_string) } +pub(super) fn api_key_from_auth_file(auth: &JsonValue) -> Option { + auth.get("OPENAI_API_KEY") + .and_then(JsonValue::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToString::to_string) +} + pub(super) fn provider_id_from_name(name: &str) -> String { let mut out = String::new(); let mut previous_was_dash = false; @@ -162,6 +176,72 @@ pub(super) fn ensure_responses_format( } } +pub(super) fn uses_auth_command(table: &Table) -> bool { + table.contains_key("auth") +} + +pub(super) fn uses_shared_openai_api_key(table: &Table) -> bool { + table + .get("requires_openai_auth") + .and_then(Item::as_bool) + .unwrap_or(false) + && !uses_auth_command(table) +} + +fn apply_credential( + table: &mut Table, + credential: &AgentProviderCredential, + api_key: Option<&str>, +) { + match credential { + AgentProviderCredential::EnvVar { name } => { + table["env_key"] = value(name.clone()); + table.remove("experimental_bearer_token"); + table.remove("requires_openai_auth"); + table.remove("auth"); + remove_authorization_header(table); + } + AgentProviderCredential::AgentStore { .. } => { + table.remove("env_key"); + table.remove("experimental_bearer_token"); + remove_authorization_header(table); + if table.get("auth").is_none() { + table["requires_openai_auth"] = value(true); + } + } + AgentProviderCredential::Inline => { + table.remove("env_key"); + table.remove("requires_openai_auth"); + table.remove("auth"); + if let Some(api_key) = api_key { + table["experimental_bearer_token"] = value(api_key.to_string()); + remove_authorization_header(table); + } + } + AgentProviderCredential::None => { + table.remove("env_key"); + table.remove("experimental_bearer_token"); + table.remove("requires_openai_auth"); + table.remove("auth"); + remove_authorization_header(table); + } + } +} + +fn remove_authorization_header(table: &mut Table) { + let remove_headers = table + .get_mut("http_headers") + .and_then(Item::as_table_like_mut) + .map(|headers| { + headers.remove(AUTHORIZATION_HEADER); + headers.is_empty() + }) + .unwrap_or(false); + if remove_headers { + table.remove("http_headers"); + } +} + fn format_from_table(table: &Table) -> Option { match string_field(table, "wire_api").as_deref() { None | Some(WIRE_API_RESPONSES) => { diff --git a/crates/inference/src/codex/mod.rs b/crates/inference/src/codex/mod.rs index 28432169..15275f74 100644 --- a/crates/inference/src/codex/mod.rs +++ b/crates/inference/src/codex/mod.rs @@ -29,6 +29,7 @@ pub const DEFAULT_PROFILE_ID: &str = "default"; #[derive(Debug, Clone)] pub struct CodexProviderAdapter { config_path: PathBuf, + auth_path: PathBuf, } /// Effective provider selection for one Codex profile. @@ -64,8 +65,14 @@ pub struct CodexProviderState { impl CodexProviderAdapter { /// Create an adapter with an explicit `config.toml` path. pub fn new(config_path: impl Into) -> Self { + let config_path = config_path.into(); + let auth_path = config_path + .parent() + .map(|parent| parent.join("auth.json")) + .unwrap_or_else(|| PathBuf::from("auth.json")); Self { - config_path: config_path.into(), + config_path, + auth_path, } } @@ -84,6 +91,11 @@ impl CodexProviderAdapter { &self.config_path } + /// Path to the Codex auth file paired with this config. + pub fn auth_path(&self) -> &Path { + &self.auth_path + } + /// Load Codex providers with effective profile selection. pub fn load_profile_state(&self) -> Result { let config = files::read_config(&self.config_path)?; @@ -120,6 +132,21 @@ impl CodexProviderAdapter { self.load_profile_state() } + /// Set the provider used by Codex's current active profile. + pub fn set_active_provider( + &self, + provider_id: &str, + ) -> Result { + let config = files::read_config(&self.config_path)?; + let active_profile_id = active_profile_id(&config); + self.set_profile_provider(&active_profile_id, provider_id) + } + + /// Clear the current active-provider override and fall back to OpenAI. + pub fn clear_active_provider(&self) -> Result { + self.set_active_provider(mapping::OPENAI_PROVIDER_ID) + } + /// Set the provider used by one Codex profile. pub fn set_profile_provider( &self, @@ -161,12 +188,15 @@ impl CodexProviderAdapter { mapping::ensure_responses_format(Some(provider.format))?; mapping::ensure_api_key(api_key)?; let provider_id = mapping::clean_provider_id(provider_id)?; - let binding = AgentProviderBinding::from_inventory( + let mut binding = AgentProviderBinding::from_inventory( provider_id.clone(), provider, AgentProviderCredential::Inline, AgentProviderSource::Custom, )?; + if let Some(api_base_url) = binding.api_base_url.as_mut() { + *api_base_url = normalize_inventory_base_url(api_base_url); + } let mut config = files::read_config(&self.config_path)?; upsert_provider(&mut config, &binding, Some(api_key))?; @@ -204,6 +234,19 @@ impl CodexProviderAdapter { let name = name.map(mapping::clean_provider_name).transpose()?; if let Some(api_key) = api_key { mapping::ensure_api_key(api_key)?; + if mapping::uses_auth_command(&provider) { + return Err( + crate::error::InferenceProviderError::InvalidAgentProviderConfig { + agent_id: AGENT_ID.to_string(), + path: self.config_path.display().to_string(), + message: format!( + "codex provider '{provider_id}' uses \ + command-backed auth and cannot be updated \ + with an inline API key" + ), + }, + ); + } } let mut binding = mapping::binding_from_table(&provider_id, &provider)?; @@ -213,6 +256,11 @@ impl CodexProviderAdapter { } upsert_provider(&mut config, &binding, api_key)?; files::write_config(&self.config_path, &config)?; + if let Some(api_key) = api_key { + if mapping::uses_shared_openai_api_key(&provider) { + self.write_shared_openai_api_key(api_key)?; + } + } Ok(self .load_providers()? @@ -233,6 +281,13 @@ impl CodexProviderAdapter { let Some(table) = provider_table(&config, &provider_id)? else { return Ok(None); }; + if mapping::uses_shared_openai_api_key(table) { + let auth = files::read_auth(&self.auth_path)?; + return Ok(mapping::api_key_from_auth_file(&auth)); + } + if mapping::uses_auth_command(table) { + return Ok(None); + } Ok(mapping::api_key_from_table(table)) } @@ -265,6 +320,23 @@ impl CodexProviderAdapter { files::write_config(&self.config_path, &config)?; Ok(removed) } + + fn write_shared_openai_api_key(&self, api_key: &str) -> Result<()> { + let mut auth = files::read_auth(&self.auth_path)?; + let Some(auth_object) = auth.as_object_mut() else { + return Err( + crate::error::InferenceProviderError::InvalidAgentCredentialStore { + agent_id: AGENT_ID.to_string(), + path: self.auth_path.display().to_string(), + message: "auth.json must contain a JSON object" + .to_string(), + }, + ); + }; + auth_object + .insert("OPENAI_API_KEY".to_string(), serde_json::json!(api_key)); + files::write_auth(&self.auth_path, &auth) + } } impl AgentProviderAdapter for CodexProviderAdapter { @@ -318,9 +390,19 @@ impl AgentProviderAdapter for CodexProviderAdapter { if let Some(selection) = &state.default_model { if let Some(provider_id) = &selection.provider_id { - config["model_provider"] = value(provider_id.clone()); + if provider_id.eq_ignore_ascii_case(mapping::OPENAI_PROVIDER_ID) + { + config.as_table_mut().remove("model_provider"); + } else { + config["model_provider"] = value(provider_id.clone()); + } + } else { + config.as_table_mut().remove("model_provider"); } config["model"] = value(selection.model_id.clone()); + } else { + config.as_table_mut().remove("model_provider"); + config.as_table_mut().remove("model"); } files::write_config(&self.config_path, &config) @@ -330,7 +412,9 @@ impl AgentProviderAdapter for CodexProviderAdapter { fn providers_from_config( config: &DocumentMut, ) -> Result> { - let mut providers = vec![mapping::built_in_openai_binding()]; + let mut providers = vec![mapping::built_in_openai_binding( + built_in_openai_api_base_url(config), + )]; if let Some(model_providers) = config.get("model_providers").and_then(Item::as_table) @@ -415,6 +499,9 @@ fn default_model_selection( let provider = config .get("model_provider") .and_then(Item::as_str) + .filter(|provider| { + !provider.eq_ignore_ascii_case(mapping::OPENAI_PROVIDER_ID) + }) .map(ToString::to_string); Some(match provider { Some(provider) => AgentModelSelection::provider_model(provider, model), @@ -422,6 +509,30 @@ fn default_model_selection( }) } +fn built_in_openai_api_base_url(config: &DocumentMut) -> Option { + config + .get("openai_base_url") + .and_then(Item::as_str) + .or_else(|| config.get("base_url").and_then(Item::as_str)) + .map(ToString::to_string) +} + +fn normalize_inventory_base_url(api_base_url: &str) -> String { + let trimmed = api_base_url.trim().trim_end_matches('/'); + let origin_only = match trimmed.split_once("://") { + Some((_scheme, rest)) => !rest.contains('/'), + None => !trimmed.contains('/'), + }; + + if trimmed.ends_with("/v1") { + trimmed.to_string() + } else if origin_only { + format!("{trimmed}/v1") + } else { + trimmed.to_string() + } +} + fn upsert_provider( config: &mut DocumentMut, binding: &AgentProviderBinding, diff --git a/crates/inference/src/codex/tests.rs b/crates/inference/src/codex/tests.rs index 56ac03ed..389e933c 100644 --- a/crates/inference/src/codex/tests.rs +++ b/crates/inference/src/codex/tests.rs @@ -13,6 +13,10 @@ fn adapter(temp: &tempfile::TempDir) -> CodexProviderAdapter { CodexProviderAdapter::new(temp.path().join("config.toml")) } +fn auth_path(temp: &tempfile::TempDir) -> std::path::PathBuf { + temp.path().join("auth.json") +} + fn provider() -> InferenceProvider { InferenceProvider { id: "inventory-id".to_string(), @@ -25,6 +29,13 @@ fn provider() -> InferenceProvider { } } +fn provider_without_versioned_base_url() -> InferenceProvider { + InferenceProvider { + api_base_url: "https://api.example.com".to_string(), + ..provider() + } +} + #[test] fn load_reads_model_providers_and_default_selection() { let temp = tempfile::tempdir().unwrap(); @@ -68,7 +79,7 @@ env_key = "OPENROUTER_API_KEY" name: "OPENROUTER_API_KEY".to_string() } ); - let default = state.default_model.unwrap(); + let default = state.default_model.clone().unwrap(); assert_eq!(default.provider_id.as_deref(), Some("openrouter")); assert_eq!(default.model_id, "openai/gpt-5.4"); } @@ -100,6 +111,74 @@ fn profile_state_defaults_to_openai_login() { assert_eq!(profile.model.as_deref(), Some("gpt-5.4")); } +#[test] +fn load_reads_openai_base_url_override() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +model = "gpt-5.4" +openai_base_url = "https://proxy.example/v1" +"#, + ) + .unwrap(); + + let state = adapter.load_providers().unwrap(); + let openai = state + .providers + .iter() + .find(|provider| provider.id == "openai") + .unwrap(); + assert_eq!( + openai.api_base_url.as_deref(), + Some("https://proxy.example/v1") + ); +} + +#[test] +fn load_canonicalizes_explicit_openai_selection() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +model_provider = "openai" +model = "gpt-5.4" +base_url = "https://legacy-openai.example/v1" +"#, + ) + .unwrap(); + + let state = adapter.load_providers().unwrap(); + let default = state.default_model.clone().unwrap(); + assert_eq!(default.provider_id, None); + assert_eq!(default.model_id, "gpt-5.4"); + + let openai = state + .providers + .iter() + .find(|provider| provider.id == "openai") + .unwrap(); + assert_eq!( + openai.api_base_url.as_deref(), + Some("https://legacy-openai.example/v1") + ); + + adapter.save_providers(&state).unwrap(); + + let config = fs::read_to_string(adapter.config_path()) + .unwrap() + .parse::() + .unwrap(); + assert!(config.get("model_provider").is_none()); + assert_eq!(config["model"].as_str(), Some("gpt-5.4")); + assert_eq!( + config["base_url"].as_str(), + Some("https://legacy-openai.example/v1") + ); +} + #[test] fn profile_state_uses_active_profile_overrides() { let temp = tempfile::tempdir().unwrap(); @@ -226,6 +305,79 @@ wire_api = "responses" assert!(config.get("model_provider").is_none()); } +#[test] +fn set_active_provider_targets_current_active_profile() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +profile = "work" +model_provider = "openai" + +[profiles.work] +model_provider = "openai" + +[model_providers.openrouter] +name = "OpenRouter" +base_url = "https://openrouter.ai/api/v1" +wire_api = "responses" +"#, + ) + .unwrap(); + + let state = adapter.set_active_provider("openrouter").unwrap(); + + assert_eq!(state.active_profile_id, "work"); + let work = state + .profiles + .iter() + .find(|profile| profile.id == "work") + .unwrap(); + assert_eq!(work.selected_provider_id, "openrouter"); + let config = fs::read_to_string(adapter.config_path()) + .unwrap() + .parse::() + .unwrap(); + assert_eq!( + config["profiles"]["work"]["model_provider"].as_str(), + Some("openrouter") + ); + assert_eq!(config["model_provider"].as_str(), Some("openai")); +} + +#[test] +fn clear_active_provider_falls_back_to_openai() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +model_provider = "openrouter" + +[model_providers.openrouter] +name = "OpenRouter" +base_url = "https://openrouter.ai/api/v1" +wire_api = "responses" +"#, + ) + .unwrap(); + + let state = adapter.clear_active_provider().unwrap(); + + let default = state + .profiles + .iter() + .find(|profile| profile.is_default) + .unwrap(); + assert_eq!(default.selected_provider_id, "openai"); + let config = fs::read_to_string(adapter.config_path()) + .unwrap() + .parse::() + .unwrap(); + assert!(config.get("model_provider").is_none()); +} + #[test] fn add_provider_writes_responses_config_and_inline_token() { let temp = tempfile::tempdir().unwrap(); @@ -255,6 +407,34 @@ fn add_provider_writes_responses_config_and_inline_token() { ); } +#[test] +fn add_provider_appends_v1_for_origin_only_base_url() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + + let binding = adapter + .add_provider( + "openrouter", + &provider_without_versioned_base_url(), + "sk-test", + ) + .unwrap(); + + assert_eq!( + binding.api_base_url.as_deref(), + Some("https://api.example.com/v1") + ); + let config = fs::read_to_string(adapter.config_path()) + .unwrap() + .parse::() + .unwrap(); + let provider = config["model_providers"]["openrouter"].as_table().unwrap(); + assert_eq!( + provider["base_url"].as_str(), + Some("https://api.example.com/v1") + ); +} + #[test] fn add_provider_rejects_chat_completion_inventory() { let temp = tempfile::tempdir().unwrap(); @@ -305,6 +485,135 @@ experimental_bearer_token = "sk-old" ); } +#[test] +fn update_provider_preserves_env_key_credentials() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +[model_providers.openrouter] +name = "OpenRouter" +base_url = "https://openrouter.ai/api/v1" +wire_api = "responses" +env_key = "OPENROUTER_API_KEY" +"#, + ) + .unwrap(); + + let binding = adapter + .update_provider("openrouter", Some("OpenRouter Team"), None) + .unwrap(); + + assert_eq!(binding.name, "OpenRouter Team"); + assert_eq!( + binding.credential, + AgentProviderCredential::EnvVar { + name: "OPENROUTER_API_KEY".to_string() + } + ); + + let content = fs::read_to_string(adapter.config_path()).unwrap(); + let config = content.parse::().unwrap(); + let provider = config["model_providers"]["openrouter"].as_table().unwrap(); + assert_eq!(provider["name"].as_str(), Some("OpenRouter Team")); + assert_eq!(provider["env_key"].as_str(), Some("OPENROUTER_API_KEY")); + assert!(provider.get("experimental_bearer_token").is_none()); +} + +#[test] +fn save_preserves_openai_auth_provider_credentials() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +model = "gpt-5.4" +model_provider = "newapi" + +[model_providers.newapi] +name = "NewAPI" +base_url = "https://api.example.com/v1" +wire_api = "responses" +requires_openai_auth = true +"#, + ) + .unwrap(); + + let state = adapter.load_providers().unwrap(); + adapter.save_providers(&state).unwrap(); + + let content = fs::read_to_string(adapter.config_path()).unwrap(); + let config = content.parse::().unwrap(); + let provider = config["model_providers"]["newapi"].as_table().unwrap(); + assert_eq!(provider["requires_openai_auth"].as_bool(), Some(true)); + assert!(provider.get("env_key").is_none()); + assert!(provider.get("experimental_bearer_token").is_none()); +} + +#[test] +fn update_provider_updates_shared_auth_json_for_openai_auth_provider() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +[model_providers.newapi] +name = "NewAPI" +base_url = "https://api.example.com/v1" +wire_api = "responses" +requires_openai_auth = true +"#, + ) + .unwrap(); + fs::write(auth_path(&temp), r#"{ "OPENAI_API_KEY": "sk-old" }"#).unwrap(); + + let binding = adapter + .update_provider("newapi", Some("New API"), Some("sk-new")) + .unwrap(); + + assert_eq!(binding.name, "New API"); + assert_eq!( + binding.credential, + AgentProviderCredential::AgentStore { + id: Some("newapi".to_string()) + } + ); + let auth: serde_json::Value = + serde_json::from_str(&fs::read_to_string(auth_path(&temp)).unwrap()) + .unwrap(); + assert_eq!(auth["OPENAI_API_KEY"].as_str(), Some("sk-new")); +} + +#[test] +fn save_clears_default_provider_for_builtin_openai_selection() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +model = "gpt-5.4" +model_provider = "openrouter" + +[model_providers.openrouter] +name = "OpenRouter" +base_url = "https://openrouter.ai/api/v1" +wire_api = "responses" +experimental_bearer_token = "sk-test" +"#, + ) + .unwrap(); + + let mut state = adapter.load_providers().unwrap(); + state.default_model = Some(AgentModelSelection::model("gpt-5.4")); + adapter.save_providers(&state).unwrap(); + + let content = fs::read_to_string(adapter.config_path()).unwrap(); + let config = content.parse::().unwrap(); + assert!(config.get("model_provider").is_none()); + assert_eq!(config["model"].as_str(), Some("gpt-5.4")); +} + #[test] fn api_key_reads_inline_token() { let temp = tempfile::tempdir().unwrap(); @@ -326,6 +635,29 @@ experimental_bearer_token = "sk-inline" ); } +#[test] +fn api_key_reads_shared_auth_json_for_openai_auth_provider() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +[model_providers.newapi] +name = "NewAPI" +base_url = "https://api.example.com/v1" +wire_api = "responses" +requires_openai_auth = true +"#, + ) + .unwrap(); + fs::write(auth_path(&temp), r#"{ "OPENAI_API_KEY": "sk-auth" }"#).unwrap(); + + assert_eq!( + adapter.api_key("newapi").unwrap(), + Some("sk-auth".to_string()) + ); +} + #[test] fn api_key_for_openai_login_provider_is_not_config_backed() { let temp = tempfile::tempdir().unwrap(); @@ -363,3 +695,43 @@ base_url = "https://openrouter.ai/api/v1" .and_then(|providers| providers.get("openrouter")) .is_none()); } + +#[test] +fn remove_provider_clears_profile_provider_references() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +model_provider = "openrouter" + +[profiles.work] +model_provider = "openrouter" + +[profiles.review] +model_provider = "openrouter" + +[model_providers.openrouter] +name = "OpenRouter" +base_url = "https://openrouter.ai/api/v1" +wire_api = "responses" +"#, + ) + .unwrap(); + + adapter.remove_provider("openrouter").unwrap(); + + let content = fs::read_to_string(adapter.config_path()).unwrap(); + let config = content.parse::().unwrap(); + assert!(config.get("model_provider").is_none()); + assert!(config["profiles"]["work"] + .as_table() + .unwrap() + .get("model_provider") + .is_none()); + assert!(config["profiles"]["review"] + .as_table() + .unwrap() + .get("model_provider") + .is_none()); +} From 63b17c14a23587effb674a60d2ad220453ec6603 Mon Sep 17 00:00:00 2001 From: akarachen Date: Wed, 29 Apr 2026 01:41:23 +0800 Subject: [PATCH 28/62] feat: better codex --- crates/api/src/dto/inference.rs | 17 +- crates/api/src/lib.rs | 5 +- crates/api/src/routes/inference.rs | 238 +++++- .../generated/dto/AgentProviderSourceDto.ts | 3 +- .../dto/ClaudeProviderStateResponse.ts | 6 +- crates/desktop/src/lib/api.ts | 32 +- crates/desktop/src/lib/locales/en.ts | 12 + crates/desktop/src/lib/locales/zh-Hans.ts | 11 + crates/desktop/src/lib/locales/zh-Hant.ts | 11 + .../desktop/src/pages/inference-providers.tsx | 4 +- .../inference-providers/claude-panel.tsx | 717 +++++++++--------- .../pages/inference-providers/codex-panel.tsx | 162 ++-- .../src/requests/inference-providers.ts | 74 +- crates/inference/inference_providers.db | Bin 0 -> 45056 bytes .../0005_create_agent_provider_bindings.sql | 25 + crates/inference/src/agent.rs | 7 + crates/inference/src/claude/mod.rs | 156 +++- crates/inference/src/claude/tests.rs | 2 +- crates/inference/src/codex/mapping.rs | 12 + crates/inference/src/codex/mod.rs | 259 ++++++- crates/inference/src/codex/tests.rs | 68 +- crates/inference/src/store.rs | 225 ++++++ 22 files changed, 1506 insertions(+), 540 deletions(-) create mode 100644 crates/inference/inference_providers.db create mode 100644 crates/inference/migrations/0005_create_agent_provider_bindings.sql diff --git a/crates/api/src/dto/inference.rs b/crates/api/src/dto/inference.rs index f610a7e4..07b758b2 100644 --- a/crates/api/src/dto/inference.rs +++ b/crates/api/src/dto/inference.rs @@ -139,6 +139,7 @@ pub enum AgentProviderSourceDto { BuiltIn, Custom, StoredCredential, + External, } impl From for AgentProviderSourceDto { @@ -148,6 +149,7 @@ impl From for AgentProviderSourceDto { AgentProviderSource::BuiltIn => Self::BuiltIn, AgentProviderSource::Custom => Self::Custom, AgentProviderSource::StoredCredential => Self::StoredCredential, + AgentProviderSource::External => Self::External, } } } @@ -344,19 +346,8 @@ pub struct UpdateCodexProfileProviderRequest { #[derive(Debug, Clone, Serialize, TS)] #[ts(export)] pub struct ClaudeProviderStateResponse { - pub api_base_url: Option, - pub api_key: Option, - pub model: Option, -} - -impl From for ClaudeProviderStateResponse { - fn from(state: aghub_inference::ClaudeConfigState) -> Self { - Self { - api_base_url: state.api_base_url, - api_key: state.api_key, - model: state.model, - } - } + pub providers: Vec, + pub active_provider_id: String, } #[derive(Debug, Deserialize, TS)] diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 872e3235..78d0a73e 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -184,7 +184,10 @@ pub async fn start(options: ApiOptions) -> Result<(), rocket::Error> { routes::inference::create_inference_provider, routes::inference::update_inference_provider, routes::inference::get_claude_state, - routes::inference::update_claude_state, + routes::inference::create_claude_provider, + routes::inference::update_claude_provider, + routes::inference::sync_claude_provider, + routes::inference::delete_claude_provider, routes::inference::clear_claude_state, routes::inference::clear_codex_state, routes::inference::delete_inference_provider, diff --git a/crates/api/src/routes/inference.rs b/crates/api/src/routes/inference.rs index 77bbc82e..025d77cc 100644 --- a/crates/api/src/routes/inference.rs +++ b/crates/api/src/routes/inference.rs @@ -13,8 +13,8 @@ use crate::dto::inference::{ CodexProviderStateResponse, CreateAgentProviderRequest, CreateInferenceProviderRequest, InferenceProviderPasswordResponse, InferenceProviderResponse, UpdateAgentProviderRequest, - UpdateClaudeProviderRequest, UpdateCodexActiveProfileRequest, - UpdateCodexProfileProviderRequest, UpdateInferenceProviderRequest, + UpdateCodexActiveProfileRequest, UpdateCodexProfileProviderRequest, + UpdateInferenceProviderRequest, }; use crate::error::{ApiCreated, ApiError, ApiNoContent, ApiResult}; use crate::state::InferenceProviderState; @@ -135,11 +135,14 @@ fn opencode_provider_response( } fn codex_provider_response( + store: &InferenceProviderStore, inventory: &[(InferenceProvider, String)], adapter: &CodexProviderAdapter, binding: AgentProviderBinding, ) -> Result { - let agent_api_key = adapter.api_key(&binding.id).map_err(ApiError::from)?; + let agent_api_key = adapter + .api_key(store, &binding.id) + .map_err(ApiError::from)?; let matched = find_matching_inventory_provider(inventory, &binding, agent_api_key)?; let response = AgentProviderResponse::from(binding); @@ -156,12 +159,14 @@ fn codex_state_response( adapter: &CodexProviderAdapter, ) -> Result { let inventory = inventory_providers_with_api_keys(store)?; - let state = adapter.load_profile_state().map_err(ApiError::from)?; + let state = adapter.load_profile_state(store).map_err(ApiError::from)?; let providers = state .providers .iter() .cloned() - .map(|binding| codex_provider_response(&inventory, adapter, binding)) + .map(|binding| { + codex_provider_response(store, &inventory, adapter, binding) + }) .collect::, _>>()?; Ok(CodexProviderStateResponse::from_state(state, providers)) } @@ -210,7 +215,9 @@ pub fn list_codex_providers( .map_err(ApiError::from)? .providers .into_iter() - .map(|binding| codex_provider_response(&inventory, &adapter, binding)) + .map(|binding| { + codex_provider_response(&store, &inventory, &adapter, binding) + }) .collect::, _>>()?; Ok(Json(providers)) } @@ -249,10 +256,10 @@ pub fn create_codex_provider( get_inventory_provider(&store, &body.inference_provider_id)?; let adapter = codex_adapter()?; let binding = adapter - .add_inventory_provider(&provider, &api_key) + .add_inventory_provider(&store, &provider, &api_key) .map_err(ApiError::from)?; adapter - .set_active_provider(&binding.id) + .set_active_provider(&store, &binding.id) .map_err(ApiError::from)?; Ok(( @@ -279,12 +286,19 @@ pub fn update_opencode_provider( #[put("/inference/agents/codex/providers/", data = "")] pub fn update_codex_provider( + state: &State, id: &str, body: Json, ) -> ApiResult { + let store = store(state); let body = body.into_inner(); let binding = codex_adapter()? - .update_provider(id, body.name.as_deref(), body.api_key.as_deref()) + .update_provider( + &store, + id, + body.name.as_deref(), + body.api_key.as_deref(), + ) .map_err(ApiError::from)?; Ok(Json(binding.into())) @@ -315,7 +329,7 @@ pub fn update_codex_profile_provider( let store = store(state); let adapter = codex_adapter()?; adapter - .set_profile_provider(profile_id, &body.provider_id) + .set_profile_provider(&store, profile_id, &body.provider_id) .map_err(ApiError::from)?; Ok(Json(codex_state_response(&store, &adapter)?)) } @@ -386,7 +400,9 @@ pub fn sync_codex_provider( "RESOURCE_NOT_FOUND", ) })?; - let agent_api_key = adapter.api_key(&binding.id).map_err(ApiError::from)?; + let agent_api_key = adapter + .api_key(&store, &binding.id) + .map_err(ApiError::from)?; let Some((provider, api_key)) = find_matching_inventory_provider(&inventory, &binding, agent_api_key)? else { @@ -419,9 +435,13 @@ pub fn delete_opencode_provider(id: &str) -> ApiNoContent { } #[delete("/inference/agents/codex/providers/")] -pub fn delete_codex_provider(id: &str) -> ApiNoContent { +pub fn delete_codex_provider( + state: &State, + id: &str, +) -> ApiNoContent { + let store = store(state); codex_adapter()? - .remove_provider(id) + .remove_provider(&store, id) .map_err(ApiError::from)?; Ok(NoContent) } @@ -490,44 +510,194 @@ pub fn delete_inference_provider( } // ============================================================================ -// Claude Code routes +// Claude Code routes (binding-table backed) // ============================================================================ +fn claude_state_response( + store: &InferenceProviderStore, + adapter: &ClaudeProviderAdapter, +) -> Result { + let state = adapter.load_bindings_state(store).map_err(ApiError::from)?; + let inventory = inventory_providers_with_api_keys(store)?; + let providers = state + .providers + .iter() + .cloned() + .map(|binding| { + let agent_api_key = store + .get_api_key( + binding.source_provider_id.as_deref().unwrap_or(""), + ) + .map_err(ApiError::from)?; + let matched = find_matching_inventory_provider( + &inventory, + &binding, + agent_api_key, + )?; + let response = AgentProviderResponse::from(binding); + let result: Result = + Ok(match matched { + Some((provider, _)) => { + response.with_matched_inference_provider(&provider) + } + None => response, + }); + result + }) + .collect::, _>>()?; + Ok(ClaudeProviderStateResponse { + providers, + active_provider_id: state + .providers + .iter() + .find(|p| { + state.default_model.as_ref().is_some_and(|m| { + p.models.iter().any(|model| model.id == m.model_id) + }) + }) + .map(|p| p.id.clone()) + .unwrap_or_default(), + }) +} + #[get("/inference/agents/claude/state")] -pub fn get_claude_state() -> ApiResult { +pub fn get_claude_state( + state: &State, +) -> ApiResult { + let store = store(state); + let adapter = claude_adapter()?; + Ok(Json(claude_state_response(&store, &adapter)?)) +} + +#[post("/inference/agents/claude/providers", data = "")] +pub fn create_claude_provider( + state: &State, + body: Json, +) -> ApiCreated { + let store = store(state); + let (provider, api_key) = + get_inventory_provider(&store, &body.inference_provider_id)?; let adapter = claude_adapter()?; - let state = adapter.load_config_state().map_err(ApiError::from)?; - Ok(Json(state.into())) + let binding = adapter + .add_binding(&store, &provider, &api_key, true) + .map_err(ApiError::from)?; + + Ok(( + Status::Created, + Json( + AgentProviderResponse::from(binding) + .with_matched_inference_provider(&provider), + ), + )) } -#[put("/inference/agents/claude/state", data = "")] -pub fn update_claude_state( - body: Json, +#[put("/inference/agents/claude/providers/", data = "")] +pub fn update_claude_provider( + state: &State, + id: &str, + body: Json, ) -> ApiResult { + let store = store(state); let adapter = claude_adapter()?; - let state = aghub_inference::ClaudeConfigState { - api_base_url: body.api_base_url.clone(), - api_key: body.api_key.clone(), - api_key_env_name: None, - model: body.model.clone(), - haiku_model: None, - sonnet_model: None, - opus_model: None, - }; - adapter.save_config_state(&state).map_err(ApiError::from)?; - Ok(Json(state.into())) + + if body.name.as_deref().is_some() { + return Err(ApiError::new( + Status::BadRequest, + "Claude provider name cannot be changed".to_string(), + "UNSUPPORTED_OPERATION", + )); + } + + if let Some(api_key) = body.api_key.as_deref() { + let row = store + .get_agent_binding("claude", id) + .map_err(ApiError::from)?; + let provider = store + .get(&row.inference_provider_id) + .map_err(ApiError::from)?; + adapter + .sync_active_binding(&provider, api_key, row.model.as_deref()) + .map_err(ApiError::from)?; + } + + Ok(Json(claude_state_response(&store, &adapter)?)) +} + +#[post("/inference/agents/claude/providers//sync")] +pub fn sync_claude_provider( + state: &State, + id: &str, +) -> ApiResult { + let store = store(state); + let adapter = claude_adapter()?; + let row = store + .get_agent_binding("claude", id) + .map_err(ApiError::from)?; + let provider = store + .get(&row.inference_provider_id) + .map_err(ApiError::from)?; + let api_key = store + .get_api_key(&provider.id) + .map_err(ApiError::from)? + .ok_or_else(|| { + ApiError::new( + Status::UnprocessableEntity, + format!( + "inference provider '{}' has no stored API key", + provider.display_name + ), + "MISSING_CREDENTIAL", + ) + })?; + + adapter + .sync_active_binding(&provider, &api_key, row.model.as_deref()) + .map_err(ApiError::from)?; + + let binding = store.binding_from_row(&row).map_err(ApiError::from)?; + Ok(Json( + AgentProviderResponse::from(binding) + .with_matched_inference_provider(&provider), + )) +} + +#[delete("/inference/agents/claude/providers/")] +pub fn delete_claude_provider( + state: &State, + id: &str, +) -> ApiNoContent { + let store = store(state); + let adapter = claude_adapter()?; + adapter.remove_binding(&store, id).map_err(ApiError::from)?; + Ok(NoContent) } #[delete("/inference/agents/claude/state")] -pub fn clear_claude_state() -> ApiNoContent { +pub fn clear_claude_state( + state: &State, +) -> ApiNoContent { + let store = store(state); let adapter = claude_adapter()?; + let rows = store + .list_agent_bindings("claude") + .map_err(ApiError::from)?; + for row in rows { + store + .delete_agent_binding("claude", &row.id) + .map_err(ApiError::from)?; + } adapter.clear_provider_config().map_err(ApiError::from)?; Ok(NoContent) } #[delete("/inference/agents/codex/state")] -pub fn clear_codex_state() -> ApiNoContent { +pub fn clear_codex_state( + state: &State, +) -> ApiNoContent { + let store = store(state); let adapter = codex_adapter()?; - adapter.clear_active_provider().map_err(ApiError::from)?; + adapter + .clear_active_provider(&store) + .map_err(ApiError::from)?; Ok(NoContent) } diff --git a/crates/desktop/src/generated/dto/AgentProviderSourceDto.ts b/crates/desktop/src/generated/dto/AgentProviderSourceDto.ts index 992e3a0d..56c233b0 100644 --- a/crates/desktop/src/generated/dto/AgentProviderSourceDto.ts +++ b/crates/desktop/src/generated/dto/AgentProviderSourceDto.ts @@ -4,4 +4,5 @@ export type AgentProviderSourceDto = | "closed_slot" | "built_in" | "custom" - | "stored_credential"; + | "stored_credential" + | "external"; diff --git a/crates/desktop/src/generated/dto/ClaudeProviderStateResponse.ts b/crates/desktop/src/generated/dto/ClaudeProviderStateResponse.ts index 5a4ef8b1..f5484e21 100644 --- a/crates/desktop/src/generated/dto/ClaudeProviderStateResponse.ts +++ b/crates/desktop/src/generated/dto/ClaudeProviderStateResponse.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AgentProviderResponse } from "./AgentProviderResponse"; export type ClaudeProviderStateResponse = { - api_base_url: string | null; - api_key: string | null; - model: string | null; + providers: Array; + active_provider_id: string; }; diff --git a/crates/desktop/src/lib/api.ts b/crates/desktop/src/lib/api.ts index dc9876ae..bc7feb8b 100644 --- a/crates/desktop/src/lib/api.ts +++ b/crates/desktop/src/lib/api.ts @@ -38,7 +38,6 @@ import type { ToolInfoDto, TransferRequest, UpdateAgentProviderRequest, - UpdateClaudeProviderRequest, UpdateCodexActiveProfileRequest, UpdateCodexProfileProviderRequest, UpdateInferenceProviderRequest, @@ -660,13 +659,38 @@ export function createApi(baseUrl: string) { getClaudeState(): Promise { return client.get("inference/agents/claude/state").json(); }, - updateClaudeState( - body: UpdateClaudeProviderRequest, + createClaude( + body: CreateAgentProviderRequest, + ): Promise { + return client + .post("inference/agents/claude/providers", { json: body }) + .json(); + }, + updateClaude( + id: string, + body: UpdateAgentProviderRequest, ): Promise { return client - .put("inference/agents/claude/state", { json: body }) + .put( + `inference/agents/claude/providers/${encodeURIComponent(id)}`, + { json: body }, + ) + .json(); + }, + syncClaude(id: string): Promise { + return client + .post( + `inference/agents/claude/providers/${encodeURIComponent(id)}/sync`, + ) .json(); }, + deleteClaude(id: string): Promise { + return client + .delete( + `inference/agents/claude/providers/${encodeURIComponent(id)}`, + ) + .then(() => undefined); + }, clearClaudeState(): Promise { return client .delete("inference/agents/claude/state") diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index eefc3ccd..d95b4655 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -135,6 +135,8 @@ export default { codexBuiltInProvider: "Built-in provider", codexBuiltInProviderInfo: "No Aghub OpenAI Responses provider has the same API Base URL and API key. This was likely added directly in Codex.", + codexProviderExternalTooltip: + "Defined in config.toml — remove it from the file to delete", codexActiveProfile: "Active Profile", codexProfileProvider: "Profile Provider", codexProfilesUsingProvider: "Profiles", @@ -175,10 +177,20 @@ export default { claudeProviderCleared: "Claude Code configuration cleared", claudeProviderClearError: "Failed to clear Claude Code configuration", claudeProviderAlreadyActive: "Already the active configuration", + claudeProviderNotInUse: "Not the current configuration", claudeUsingOfficialApi: "Using Anthropic official API with browser login.", clearClaudeProvider: "Clear Claude Code Configuration", clearClaudeProviderConfirm: "Remove all custom API settings and fall back to Anthropic's official API?", + deleteClaudeProvider: "Delete Claude Code Provider", + deleteClaudeProviderConfirm: 'Delete "{{name}}" from Claude Code?', + refreshClaudeProviders: "Refresh Claude Code providers", + claudeConfigProvider: "Config provider", + claudeProviderDeleted: "Claude Code provider deleted", + claudeProviderDeleteError: "Failed to delete Claude Code provider", + claudeProviderSynced: "Claude Code provider synced", + claudeProviderSyncError: "Failed to sync Claude Code provider", + claudeNoProfiles: "No profiles needed for Claude Code.", agentProviderSourceCustom: "Custom", agentProviderSourceBuiltIn: "Built-in", agentProviderSourceClosedSlot: "Closed slot", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index 37e7750b..e6373def 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -130,6 +130,7 @@ export default { codexBuiltInProvider: "内置 provider", codexBuiltInProviderInfo: "未在 Aghub OpenAI Responses Provider 中找到相同 API Base URL 和 API key,疑似用户直接在 Codex 里添加。", + codexProviderExternalTooltip: "在 config.toml 中定义 — 需手动编辑文件删除", codexActiveProfile: "当前 Profile", codexProfileProvider: "Profile Provider", codexProfilesUsingProvider: "使用中的 Profile", @@ -168,10 +169,20 @@ export default { claudeProviderCleared: "Claude Code 配置已清除", claudeProviderClearError: "清除 Claude Code 配置失败", claudeProviderAlreadyActive: "已经是当前配置", + claudeProviderNotInUse: "当前未使用此配置", claudeUsingOfficialApi: "使用 Anthropic 官方 API(浏览器登录)。", clearClaudeProvider: "清除 Claude Code 配置", clearClaudeProviderConfirm: "移除所有自定义 API 设置并回退到 Anthropic 官方 API?", + deleteClaudeProvider: "删除 Claude Code Provider", + deleteClaudeProviderConfirm: '确定从 Claude Code 删除"{{name}}"吗?', + refreshClaudeProviders: "刷新 Claude Code Provider", + claudeConfigProvider: "配置文件 provider", + claudeProviderDeleted: "Claude Code Provider 已删除", + claudeProviderDeleteError: "删除 Claude Code Provider 失败", + claudeProviderSynced: "Claude Code Provider 已同步", + claudeProviderSyncError: "同步 Claude Code Provider 失败", + claudeNoProfiles: "Claude Code 不需要 profile。", agentProviderSourceCustom: "自定义", agentProviderSourceBuiltIn: "内置", agentProviderSourceClosedSlot: "封闭槽位", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index 21c9c3ec..bd741864 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -130,6 +130,7 @@ export default { codexBuiltInProvider: "內建 provider", codexBuiltInProviderInfo: "未在 Aghub OpenAI Responses Provider 中找到相同 API Base URL 和 API key,疑似使用者直接在 Codex 裡新增。", + codexProviderExternalTooltip: "在 config.toml 中定義 — 需手動編輯檔案刪除", codexActiveProfile: "目前 Profile", codexProfileProvider: "Profile Provider", codexProfilesUsingProvider: "使用中的 Profile", @@ -168,10 +169,20 @@ export default { claudeProviderCleared: "Claude Code 配置已清除", claudeProviderClearError: "清除 Claude Code 配置失敗", claudeProviderAlreadyActive: "已是目前配置", + claudeProviderNotInUse: "當前未使用此配置", claudeUsingOfficialApi: "使用 Anthropic 官方 API(瀏覽器登入)。", clearClaudeProvider: "清除 Claude Code 配置", clearClaudeProviderConfirm: "移除所有自訂 API 設定並回退到 Anthropic 官方 API?", + deleteClaudeProvider: "刪除 Claude Code Provider", + deleteClaudeProviderConfirm: "確定從 Claude Code 刪除「{{name}}」嗎?", + refreshClaudeProviders: "重新整理 Claude Code Provider", + claudeConfigProvider: "設定檔 provider", + claudeProviderDeleted: "Claude Code Provider 已刪除", + claudeProviderDeleteError: "刪除 Claude Code Provider 失敗", + claudeProviderSynced: "Claude Code Provider 已同步", + claudeProviderSyncError: "同步 Claude Code Provider 失敗", + claudeNoProfiles: "Claude Code 不需要 profile。", agentProviderSourceCustom: "自訂", agentProviderSourceBuiltIn: "內建", agentProviderSourceClosedSlot: "封閉槽位", diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index 5caed805..4a30ebf2 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -1175,7 +1175,9 @@ export default function InferenceProvidersPage() { )} {panel.type === "agent" && panel.agentId === "claude" && ( - + )} {panel.type === "create" && ( diff --git a/crates/desktop/src/pages/inference-providers/claude-panel.tsx b/crates/desktop/src/pages/inference-providers/claude-panel.tsx index dc446755..ce5f059e 100644 --- a/crates/desktop/src/pages/inference-providers/claude-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/claude-panel.tsx @@ -1,10 +1,10 @@ import { ArrowPathIcon, CheckCircleIcon, - KeyIcon, PlayIcon, PlusIcon, ServerIcon, + TrashIcon, } from "@heroicons/react/24/solid"; import { Alert, @@ -20,22 +20,196 @@ import { toast, } from "@heroui/react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import type { FormEvent } from "react"; import { useTranslation } from "react-i18next"; -import type { InferenceProviderResponse } from "../../generated/dto"; +import type { + AgentProviderResponse, + InferenceProviderResponse, +} from "../../generated/dto"; import { useApi } from "../../hooks/use-api"; import { AgentIcon } from "../../lib/agent-icons"; import { cn } from "../../lib/utils"; import { clearClaudeProviderMutationOptions, claudeProviderStateQueryOptions, + createClaudeProviderMutationOptions, + deleteClaudeProviderMutationOptions, inferenceProviderListQueryOptions, + syncClaudeProviderMutationOptions, updateClaudeProviderMutationOptions, } from "../../requests/inference-providers"; -function sameApiBaseUrl(left: string, right: string) { - return left.trim().replace(/\/+$/, "") === right.trim().replace(/\/+$/, ""); +function ClaudeCreateProviderDialog({ + isOpen, + inventoryProviders, + isInventoryLoading, + onClose, +}: { + isOpen: boolean; + inventoryProviders: InferenceProviderResponse[]; + isInventoryLoading: boolean; + onClose: () => void; +}) { + const { t } = useTranslation(); + const api = useApi(); + const queryClient = useQueryClient(); + const [selectedProviderId, setSelectedProviderId] = useState(""); + + const anthropicProviders = useMemo( + () => + inventoryProviders.filter( + (provider) => provider.format === "anthropic", + ), + [inventoryProviders], + ); + const defaultProviderId = anthropicProviders[0]?.id ?? ""; + + useEffect(() => { + if (!isOpen) return; + setSelectedProviderId((current) => current || defaultProviderId); + }, [defaultProviderId, isOpen]); + + const createMutation = useMutation({ + ...createClaudeProviderMutationOptions({ + api, + queryClient, + onSuccess: async () => { + toast.success(t("claudeProviderUpdated")); + onClose(); + }, + }), + }); + + const activeError = createMutation.error; + const isPending = createMutation.isPending; + const hasAnthropicProviders = anthropicProviders.length > 0; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + if (!selectedProviderId) return; + + createMutation.mutate({ + inference_provider_id: selectedProviderId, + }); + }; + + return ( + { + if (!open) onClose(); + }} + > + + + + + + {t("createClaudeProvider")} + + +
+ + {activeError && ( + + + + + {activeError instanceof Error + ? activeError.message + : String(activeError)} + + + + )} + + {isInventoryLoading && ( +
+ +
+ )} + + {!isInventoryLoading && !hasAnthropicProviders && ( + + + + + {t("noInferenceProvidersForClaude")} + + + + )} + + {!isInventoryLoading && hasAnthropicProviders && ( + + )} +
+ + + + +
+
+
+
+ ); } function ClaudeOfficialRow({ @@ -98,27 +272,30 @@ function ClaudeOfficialRow({ } function ClaudeProviderRow({ - label, - apiBaseUrl, - apiKey, - model, + provider, isActive, isSyncing, - onActivate, + isSelecting, + isDeleting, + canSelect, + onSelect, onSync, - showSync, + onDelete, }: { - label: string; - apiBaseUrl: string | null; - apiKey: string | null; - model: string | null; + provider: AgentProviderResponse; isActive: boolean; isSyncing: boolean; - onActivate: () => void; + isSelecting: boolean; + isDeleting: boolean; + canSelect: boolean; + onSelect: () => void; onSync: () => void; - showSync: boolean; + onDelete: () => void; }) { const { t } = useTranslation(); + const matchedProvider = provider.matched_inference_provider; + const label = matchedProvider?.display_name ?? provider.name; + const model = provider.models[0]?.id ?? null; return (
@@ -134,13 +311,16 @@ function ClaudeProviderRow({ )}
- {apiBaseUrl && ( - {apiBaseUrl} - )} - {apiKey && ( - - - *** + + {matchedProvider + ? `${t("providerModels")}: ${ + matchedProvider.model_count + }` + : t("claudeConfigProvider")} + + {provider.api_base_url && ( + + {provider.api_base_url} )} {model && {model}} @@ -148,7 +328,7 @@ function ClaudeProviderRow({
- {showSync && ( + {matchedProvider && ( + + {t("delete")} + + + + @@ -189,7 +386,9 @@ function ClaudeProviderRow({ {isActive ? t("claudeProviderAlreadyActive") - : t("enable")} + : !canSelect + ? t("claudeNoProfiles") + : t("enable")}
@@ -197,78 +396,64 @@ function ClaudeProviderRow({ ); } -export function ClaudeInferenceProviderPanel() { +export function ClaudeInferenceProviderPanel(_: { + onEditInferenceProvider: (providerName: string) => void; +}) { const { t } = useTranslation(); const api = useApi(); const queryClient = useQueryClient(); const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [selectedProviderId, setSelectedProviderId] = useState(""); + const [deleteTarget, setDeleteTarget] = + useState(null); const { - data: state, + data: claudeState, isLoading, isFetching, refetch, } = useQuery({ ...claudeProviderStateQueryOptions({ api }), }); - const { data: inventoryProviders = [], isLoading: isInventoryLoading } = useQuery({ ...inferenceProviderListQueryOptions({ api }), }); - const anthropicProviders = useMemo( - () => - inventoryProviders.filter( - (provider) => provider.format === "anthropic", - ), - [inventoryProviders], - ); + const activeProviderId = + (claudeState as { active_provider_id?: string } | undefined) + ?.active_provider_id ?? ""; + const isOfficialActive = activeProviderId === ""; + const customProviders = + (claudeState as { providers?: AgentProviderResponse[] } | undefined) + ?.providers ?? []; - const { data: matchedProvider } = useQuery({ - queryKey: [ - "claude-provider-match", - state?.api_base_url, - state?.api_key, - ], - queryFn: async () => { - if (!state?.api_base_url || !state?.api_key) { - return undefined; - } - const currentApiBaseUrl = state.api_base_url; - const candidate = inventoryProviders.find((p) => - sameApiBaseUrl(p.api_base_url, currentApiBaseUrl), + const clearMutation = useMutation({ + ...clearClaudeProviderMutationOptions({ + api, + queryClient, + onSuccess: async () => { + toast.success(t("claudeProviderCleared")); + }, + }), + onError: (error) => { + console.error("Failed to clear Claude provider:", error); + toast.danger( + error instanceof Error + ? error.message + : t("claudeProviderClearError"), ); - if (!candidate) return undefined; - try { - const password = await api.inferenceProviders.getPassword( - candidate.name, - ); - return password.api_key === state.api_key - ? candidate - : undefined; - } catch { - return undefined; - } }, - enabled: Boolean(state?.api_base_url && state?.api_key), - staleTime: 0, }); - - const updateMutation = useMutation({ + const selectProviderMutation = useMutation({ ...updateClaudeProviderMutationOptions({ api, queryClient, onSuccess: async () => { toast.success(t("claudeProviderUpdated")); - setIsAddDialogOpen(false); - setSelectedProviderId(""); }, }), onError: (error) => { - console.error("Failed to update Claude provider:", error); + console.error("Failed to switch Claude provider:", error); toast.danger( error instanceof Error ? error.message @@ -276,85 +461,41 @@ export function ClaudeInferenceProviderPanel() { ); }, }); - - const clearMutation = useMutation({ - ...clearClaudeProviderMutationOptions({ + const deleteMutation = useMutation({ + ...deleteClaudeProviderMutationOptions({ api, queryClient, onSuccess: async () => { - setIsDeleteDialogOpen(false); - toast.success(t("claudeProviderCleared")); + setDeleteTarget(null); + toast.success(t("claudeProviderDeleted")); }, }), onError: (error) => { - console.error("Failed to clear Claude provider:", error); + console.error("Failed to delete Claude provider:", error); toast.danger( error instanceof Error ? error.message - : t("claudeProviderClearError"), + : t("claudeProviderDeleteError"), ); }, }); - - const hasCustomConfig = Boolean( - state?.api_base_url || state?.api_key || state?.model, - ); - const hasUnmatchedCustomConfig = hasCustomConfig && !matchedProvider; - - const activateProvider = async (target: InferenceProviderResponse) => { - if (target.format !== "anthropic") return; - const provider = inventoryProviders.find((p) => p.id === target.id); - if (!provider) return; - - try { - const password = await api.inferenceProviders.getPassword( - provider.name, - ); - updateMutation.mutate({ - api_base_url: provider.api_base_url, - api_key: password.api_key, - model: provider.models[0] ?? null, - }); - } catch (error) { - console.error("Failed to get provider password:", error); - toast.danger( - error instanceof Error - ? error.message - : t("inferenceProviderPasswordLoadFailed"), - ); - } - }; - - const handleAdd = async (event: FormEvent) => { - event.preventDefault(); - if (!selectedProviderId) return; - const provider = inventoryProviders.find( - (p) => p.id === selectedProviderId, - ); - if (!provider) return; - await activateProvider(provider); - }; - - const handleSync = async () => { - if (!matchedProvider) return; - try { - const password = await api.inferenceProviders.getPassword( - matchedProvider.name, - ); - updateMutation.mutate({ - api_base_url: matchedProvider.api_base_url, - api_key: password.api_key, - model: matchedProvider.models[0] ?? state?.model ?? null, - }); - } catch (error) { + const syncMutation = useMutation({ + ...syncClaudeProviderMutationOptions({ + api, + queryClient, + onSuccess: async () => { + toast.success(t("claudeProviderSynced")); + }, + }), + onError: (error) => { console.error("Failed to sync Claude provider:", error); toast.danger( error instanceof Error ? error.message - : t("claudeProviderUpdateError"), + : t("claudeProviderSyncError"), ); - } - }; + }, + }); return ( <> @@ -376,20 +517,30 @@ export function ClaudeInferenceProviderPanel() {
- + + + + + + {t("refresh")} + +
)} @@ -477,150 +621,18 @@ export function ClaudeInferenceProviderPanel() {
- { - if (!open) { - setIsAddDialogOpen(false); - setSelectedProviderId(""); - } - }} - > - - - - - - {t("createClaudeProvider")} - - -
- - {updateMutation.error && ( - - - - - {updateMutation.error instanceof - Error - ? updateMutation.error - .message - : String( - updateMutation.error, - )} - - - - )} - - {isInventoryLoading && ( -
- -
- )} - - {!isInventoryLoading && - anthropicProviders.length === 0 && ( - - - - - {t( - "noInferenceProvidersForClaude", - )} - - - - )} - - {!isInventoryLoading && - anthropicProviders.length > 0 && ( - - )} -
- - - - -
-
-
-
+ inventoryProviders={inventoryProviders} + isInventoryLoading={isInventoryLoading} + onClose={() => setIsAddDialogOpen(false)} + /> setIsDeleteDialogOpen(false)} + isOpen={Boolean(deleteTarget)} + onOpenChange={(open) => { + if (!open) setDeleteTarget(null); + }} > @@ -628,25 +640,30 @@ export function ClaudeInferenceProviderPanel() { - {t("clearClaudeProvider")} + {t("deleteClaudeProvider")} - {t("clearClaudeProviderConfirm")} + {t("deleteClaudeProviderConfirm", { + name: deleteTarget?.name, + })} diff --git a/crates/desktop/src/pages/inference-providers/codex-panel.tsx b/crates/desktop/src/pages/inference-providers/codex-panel.tsx index 4c98bf0d..c24218be 100644 --- a/crates/desktop/src/pages/inference-providers/codex-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/codex-panel.tsx @@ -273,7 +273,6 @@ function CodexOfficialRow({ function CodexProviderRow({ provider, - model, isActive, isSyncing, isSelecting, @@ -284,7 +283,6 @@ function CodexProviderRow({ onDelete, }: { provider: AgentProviderResponse; - model: string | null; isActive: boolean; isSyncing: boolean; isSelecting: boolean; @@ -297,6 +295,8 @@ function CodexProviderRow({ const { t } = useTranslation(); const matchedProvider = provider.matched_inference_provider; const label = matchedProvider?.display_name ?? provider.name; + const model = provider.models[0]?.id ?? null; + const isExternal = provider.source === "external"; return (
@@ -329,7 +329,7 @@ function CodexProviderRow({
- {matchedProvider && ( + {matchedProvider && !isExternal && ( + + + {isExternal + ? t("codexProviderExternalTooltip") + : t("delete")} + + - - {t("delete")} -
); @@ -568,84 +577,47 @@ export function CodexInferenceProviderPanel(_: { clearMutation.mutate() } /> - {customProviders.length === 0 ? ( -
-

- {t("noCodexProviders")} -

- -
- ) : ( - customProviders.map((provider) => ( - ( + { - if (!activeProfile) return; - selectProviderMutation.mutate( - { - profileId: - activeProfile.id, - body: { - provider_id: - provider.id, - }, - }, - ); - }} - onSync={() => - syncMutation.mutate( - provider.id, - ) - } - onDelete={() => - setDeleteTarget(provider) - } - /> - )) - )} + } + isDeleting={ + deleteMutation.isPending && + deleteTarget?.id === provider.id + } + canSelect={Boolean(activeProfile)} + onSelect={() => { + if (!activeProfile) return; + selectProviderMutation.mutate({ + profileId: activeProfile.id, + body: { + provider_id: + provider.id, + }, + }); + }} + onSync={() => + syncMutation.mutate(provider.id) + } + onDelete={() => + setDeleteTarget(provider) + } + /> + ))}
)} diff --git a/crates/desktop/src/requests/inference-providers.ts b/crates/desktop/src/requests/inference-providers.ts index a25a4274..2fad89b3 100644 --- a/crates/desktop/src/requests/inference-providers.ts +++ b/crates/desktop/src/requests/inference-providers.ts @@ -10,7 +10,6 @@ import type { CreateInferenceProviderRequest, InferenceProviderResponse, UpdateAgentProviderRequest, - UpdateClaudeProviderRequest, UpdateCodexActiveProfileRequest, UpdateCodexProfileProviderRequest, UpdateInferenceProviderRequest, @@ -522,6 +521,27 @@ export async function invalidateClaudeProviderQueries( }); } +interface CreateClaudeProviderMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: (data: AgentProviderResponse) => void | Promise; +} + +export function createClaudeProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: CreateClaudeProviderMutationParams) { + return mutationOptions({ + mutationFn: (body: CreateAgentProviderRequest) => + api.inferenceProviders.createClaude(body), + onSuccess: async (data) => { + await invalidateClaudeProviderQueries(queryClient); + await onSuccess?.(data); + }, + }); +} + interface UpdateClaudeProviderMutationParams { api: ApiClient; queryClient: QueryClient; @@ -534,8 +554,56 @@ export function updateClaudeProviderMutationOptions({ onSuccess, }: UpdateClaudeProviderMutationParams) { return mutationOptions({ - mutationFn: (body: UpdateClaudeProviderRequest) => - api.inferenceProviders.updateClaudeState(body), + mutationFn: ({ + id, + body, + }: { + id: string; + body: UpdateAgentProviderRequest; + }) => api.inferenceProviders.updateClaude(id, body), + onSuccess: async () => { + await invalidateClaudeProviderQueries(queryClient); + await onSuccess?.(); + }, + }); +} + +interface SyncClaudeProviderMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: ( + data: AgentProviderResponse, + id: string, + ) => void | Promise; +} + +export function syncClaudeProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: SyncClaudeProviderMutationParams) { + return mutationOptions({ + mutationFn: (id: string) => api.inferenceProviders.syncClaude(id), + onSuccess: async (data, id) => { + await invalidateClaudeProviderQueries(queryClient); + await onSuccess?.(data, id); + }, + }); +} + +interface DeleteClaudeProviderMutationParams { + api: ApiClient; + queryClient: QueryClient; + onSuccess?: () => void | Promise; +} + +export function deleteClaudeProviderMutationOptions({ + api, + queryClient, + onSuccess, +}: DeleteClaudeProviderMutationParams) { + return mutationOptions({ + mutationFn: (id: string) => api.inferenceProviders.deleteClaude(id), onSuccess: async () => { await invalidateClaudeProviderQueries(queryClient); await onSuccess?.(); diff --git a/crates/inference/inference_providers.db b/crates/inference/inference_providers.db new file mode 100644 index 0000000000000000000000000000000000000000..ee9aaa5753eced47e1707247e0739ec7181fa13b GIT binary patch literal 45056 zcmeI&eQXnD90%~bzN{}}t&l-gba@2HR^80D0^&x5Zae0(t{q)xSO%O&?-=LWZuYvd zA!67Fl8^vO4AEeWfAEJIjEOH8qAnRqBF4pFf^jo3BpRa;gM=6)`0_m0wQIXph>3q# zzfZFEde7^9o?oBso?F*OpPEp}_JMd*N)Qis8^`n9dO|pkE1mtJZ+f!QSJUhVw=nyN~kSLJG=rh0Ynz1?PU1?V_EHy`$D9ab{-v&l}Ki3cFdZk zdEvMsB@|htfAY6d+tuuC5$x`9 zbNYg(JrSX|N}@EB7|=h9xhi5^?iq_?Eq90o<5+)=Yih6=wCPj-S}=B4OI%W8q01?- zJc@M%GHlLk9J?pv1XGEt$5W`;Ig|7SW1lAKV2`G8{VOW`?aAcY4Qb{a`KcBB|OAN zC9zH!H=phmWAOHafKmY;| zfB*y_009UTp+LlJ;oCg6h|!$f6HM-ZlGI%}No_sX*VeDDsa;u9zlPL3*x;#eSXEno zZui4=T*dOMa`c6-X3jr(^2SFKJzIY|{#pB{-+!Q9-2KK&n=4LNoLD^E{?KOc>sNLy zI`{5YlZCHgy_qCgPHj!;8#;N~@16ems7G}SP9NQ3YB)V%J^bCe-Q$n1uy(gDJMh-Q z>7L-3g`=w{mYM_G@0(fs<%;(&&BW6ti9M( zfBEO*26<+DeSQCRgZNJ9%(9^~jY}`y?VdVuvU&g2$;nlYrn7gJ_g0jDu|_di_(s-) zVb&|Xy+M+jUwOKDzO!2EwO)5}iL(bfk9@e}%A20)yPiE{9}A8ydHJ=er+>H}xX^B$ zxYu>_lT)*d&8_O3S8F=;ZqfedM|Jy`E$NPyzk>|~3_G&V*SEG@=8Vp9G&BmVp(|`I$f&c^{009U< z00Izz00bZa0SG{#hy~d5f875U@dV@2AOHafKmY;|fB*y_009U<00LA1&;Jny5P$## zAOHafKmY;|fB*y_0D {} } if let Some(capability) = diff --git a/crates/inference/src/claude/mod.rs b/crates/inference/src/claude/mod.rs index abd16c5a..b8b5631e 100644 --- a/crates/inference/src/claude/mod.rs +++ b/crates/inference/src/claude/mod.rs @@ -2,6 +2,14 @@ //! //! Claude Code stores provider configuration in `~/.claude/settings.json` //! via environment variable overrides in the `env` object. +//! +//! Because Claude does not natively support multiple providers, aghub uses +//! the `agent_provider_bindings` SQLite table as a workaround. Each binding +//! row maps an inference provider to Claude; the active binding is the one +//! whose `is_active` flag is set. When a binding is active, its provider's +//! URL, API key, and model are written to Claude's `settings.json` env +//! object. Switching providers simply swaps which binding is active and +//! rewrites the env block. mod files; @@ -19,9 +27,10 @@ use crate::agent::{ AgentProviderDefaultSupport, AgentProviderSource, AgentProviderState, }; use crate::error::Result; +use crate::model::InferenceProvider; +use crate::store::{InferenceProviderRepository, InferenceProviderStore}; pub(super) const AGENT_ID: &str = "claude"; -pub(super) const PRIMARY_PROVIDER_ID: &str = "primary"; const API_BASE_URL_ENV: &str = "ANTHROPIC_BASE_URL"; const API_KEY_ENV: &str = "ANTHROPIC_API_KEY"; @@ -189,6 +198,143 @@ impl ClaudeProviderAdapter { files::write_config(&self.config_path, &config)?; Ok(()) } + + /// Load bindings from the SQLite store and sync the active one into + /// `settings.json`. Returns the effective provider state. + pub fn load_bindings_state( + &self, + store: &InferenceProviderStore, + ) -> Result { + let rows = store.list_agent_bindings(AGENT_ID)?; + let active_row = rows.iter().find(|r| r.is_active); + + // If there is an active binding but settings.json does not match, + // rewrite settings.json now so the agent sees the correct config. + if let Some(row) = active_row { + let provider = store.get(&row.inference_provider_id)?; + let api_key = + store.get_api_key(&provider.id)?.ok_or_else(|| { + crate::error::InferenceProviderError::NotFound( + provider.id.clone(), + ) + })?; + self.sync_active_binding( + &provider, + &api_key, + row.model.as_deref(), + )?; + } else if active_row.is_none() && self.has_custom_config()? { + // No active binding but settings.json still has overrides; + // clear them so Claude falls back to official API. + self.clear_provider_config()?; + } + + let providers = rows + .iter() + .map(|row| store.binding_from_row(row)) + .collect::>>()?; + + let default_model = active_row + .and_then(|r| r.model.clone()) + .map(AgentModelSelection::model); + + Ok(AgentProviderState { + providers, + default_model, + small_model: None, + }) + } + + /// Add a new binding, optionally making it active. + pub fn add_binding( + &self, + store: &InferenceProviderStore, + provider: &InferenceProvider, + api_key: &str, + set_active: bool, + ) -> Result { + let model = provider.models.first().cloned(); + let row = store.create_agent_binding( + AGENT_ID, + &provider.id, + model.as_deref(), + set_active, + )?; + + if set_active { + self.sync_active_binding(provider, api_key, model.as_deref())?; + } + + store.binding_from_row(&row) + } + + /// Switch the active binding. + pub fn set_active_binding( + &self, + store: &InferenceProviderStore, + binding_id: &str, + ) -> Result { + store.update_agent_binding(AGENT_ID, binding_id, Some(true), None)?; + let row = store + .list_agent_bindings(AGENT_ID)? + .into_iter() + .find(|r| r.id == binding_id) + .ok_or_else(|| { + crate::error::InferenceProviderError::NotFound( + binding_id.to_string(), + ) + })?; + + let provider = store.get(&row.inference_provider_id)?; + let api_key = store.get_api_key(&provider.id)?.ok_or_else(|| { + crate::error::InferenceProviderError::NotFound(provider.id.clone()) + })?; + self.sync_active_binding(&provider, &api_key, row.model.as_deref())?; + + self.load_bindings_state(store) + } + + /// Remove a binding. If it was active, clear settings.json. + pub fn remove_binding( + &self, + store: &InferenceProviderStore, + binding_id: &str, + ) -> Result { + let row = store.get_agent_binding(AGENT_ID, binding_id)?; + let binding = store.binding_from_row(&row)?; + store.delete_agent_binding(AGENT_ID, binding_id)?; + + if row.is_active { + self.clear_provider_config()?; + } + + Ok(binding) + } + + /// Sync an inventory provider into settings.json as the active config. + pub fn sync_active_binding( + &self, + provider: &InferenceProvider, + api_key: &str, + model: Option<&str>, + ) -> Result<()> { + let state = ClaudeConfigState { + api_base_url: Some(provider.api_base_url.clone()), + api_key: Some(api_key.to_string()), + api_key_env_name: Some(AUTH_TOKEN_ENV.to_string()), + model: model.map(ToString::to_string), + haiku_model: None, + sonnet_model: None, + opus_model: None, + }; + self.save_config_state(&state) + } + + /// Whether settings.json currently contains any custom provider config. + fn has_custom_config(&self) -> Result { + let state = self.load_config_state()?; + Ok(state.api_base_url.is_some() || state.api_key.is_some()) + } } impl AgentProviderAdapter for ClaudeProviderAdapter { @@ -197,20 +343,24 @@ impl AgentProviderAdapter for ClaudeProviderAdapter { } fn capabilities(&self) -> AgentProviderCapabilities { - AgentProviderCapabilities::closed_single( + AgentProviderCapabilities::registry( AgentProviderDefaultSupport::MODEL_ONLY, AgentCredentialSupport::ENV_VAR, + crate::agent::BuiltInProviderSupport::NONE, ) } fn load_providers(&self) -> Result { + // When called without a store (generic adapter interface), fall back + // to reading settings.json directly and return a single closed-slot + // provider for backward compatibility. let state = self.load_config_state()?; let provider = if state.api_base_url.is_some() || state.api_key.is_some() { Some(AgentProviderBinding { - id: PRIMARY_PROVIDER_ID.to_string(), + id: "primary".to_string(), source_provider_id: None, name: "Custom".to_string(), format: Some(crate::model::InferenceProviderFormat::Anthropic), diff --git a/crates/inference/src/claude/tests.rs b/crates/inference/src/claude/tests.rs index 36032557..41eb5b98 100644 --- a/crates/inference/src/claude/tests.rs +++ b/crates/inference/src/claude/tests.rs @@ -53,7 +53,7 @@ fn load_reads_auth_token_and_normalized_model_state() { assert_eq!(state.default_model.unwrap().model_id, "claude-top"); let provider = &state.providers[0]; - assert_eq!(provider.id, PRIMARY_PROVIDER_ID); + assert_eq!(provider.id, "primary"); assert_eq!(provider.name, "Custom"); assert_eq!(provider.source, AgentProviderSource::ClosedSlot); assert_eq!(provider.format, Some(InferenceProviderFormat::Anthropic)); diff --git a/crates/inference/src/codex/mapping.rs b/crates/inference/src/codex/mapping.rs index 133170ba..ef2c016d 100644 --- a/crates/inference/src/codex/mapping.rs +++ b/crates/inference/src/codex/mapping.rs @@ -59,6 +59,18 @@ pub(super) fn binding_from_table( }) } +/// Like `binding_from_table` but marks the source as External. +/// +/// Used for providers read from config.toml that aghub does not manage. +pub(super) fn binding_from_table_external( + provider_id: &str, + table: &Table, +) -> Result { + let mut binding = binding_from_table(provider_id, table)?; + binding.source = AgentProviderSource::External; + Ok(binding) +} + pub(super) fn provider_table_from_binding( binding: &AgentProviderBinding, api_key: Option<&str>, diff --git a/crates/inference/src/codex/mod.rs b/crates/inference/src/codex/mod.rs index 15275f74..81f1bf78 100644 --- a/crates/inference/src/codex/mod.rs +++ b/crates/inference/src/codex/mod.rs @@ -21,6 +21,7 @@ use crate::agent::{ }; use crate::error::Result; use crate::model::InferenceProvider; +use crate::store::{InferenceProviderRepository, InferenceProviderStore}; pub(super) const AGENT_ID: &str = "codex"; pub const DEFAULT_PROFILE_ID: &str = "default"; @@ -97,9 +98,17 @@ impl CodexProviderAdapter { } /// Load Codex providers with effective profile selection. - pub fn load_profile_state(&self) -> Result { + /// + /// Merges providers from three sources: + /// 1. OpenAI built-in provider + /// 2. config.toml `[model_providers]` (marked as External) + /// 3. Binding table providers (marked as Custom) + pub fn load_profile_state( + &self, + store: &InferenceProviderStore, + ) -> Result { let config = files::read_config(&self.config_path)?; - let providers = providers_from_config(&config)?; + let providers = self.load_all_providers(store, &config)?; let active_profile_id = active_profile_id(&config); let profiles = profile_ids(&config, &active_profile_id) .into_iter() @@ -129,27 +138,33 @@ impl CodexProviderAdapter { } files::write_config(&self.config_path, &config)?; - self.load_profile_state() + let store = InferenceProviderStore::new(""); + self.load_profile_state(&store) } /// Set the provider used by Codex's current active profile. pub fn set_active_provider( &self, + store: &InferenceProviderStore, provider_id: &str, ) -> Result { let config = files::read_config(&self.config_path)?; let active_profile_id = active_profile_id(&config); - self.set_profile_provider(&active_profile_id, provider_id) + self.set_profile_provider(store, &active_profile_id, provider_id) } /// Clear the current active-provider override and fall back to OpenAI. - pub fn clear_active_provider(&self) -> Result { - self.set_active_provider(mapping::OPENAI_PROVIDER_ID) + pub fn clear_active_provider( + &self, + store: &InferenceProviderStore, + ) -> Result { + self.set_active_provider(store, mapping::OPENAI_PROVIDER_ID) } /// Set the provider used by one Codex profile. pub fn set_profile_provider( &self, + store: &InferenceProviderStore, profile_id: &str, provider_id: &str, ) -> Result { @@ -157,22 +172,47 @@ impl CodexProviderAdapter { let provider_id = clean_selected_provider_id(provider_id)?; let mut config = files::read_config(&self.config_path)?; ensure_profile_exists(&config, &profile_id)?; - ensure_selectable_provider(&config, &provider_id)?; + + // Check if provider is selectable (built-in, config.toml, or binding) + let all_providers = self.load_all_providers(store, &config)?; + let is_selectable = provider_id + .eq_ignore_ascii_case(mapping::OPENAI_PROVIDER_ID) + || all_providers.iter().any(|p| p.id == provider_id); + if !is_selectable { + return Err(crate::error::InferenceProviderError::NotFound( + provider_id.to_string(), + )); + } if profile_id == DEFAULT_PROFILE_ID { let table = config.as_table_mut(); if provider_id == mapping::OPENAI_PROVIDER_ID { table.remove("model_provider"); } else { - table["model_provider"] = value(provider_id); + table["model_provider"] = value(provider_id.clone()); } } else { let profile = profile_table_mut(&mut config, &profile_id)?; - profile["model_provider"] = value(provider_id); + profile["model_provider"] = value(provider_id.clone()); + } + + // If selecting a binding provider, also write its details to config.toml + if let Some(binding) = + all_providers.iter().find(|p| p.id == provider_id) + { + if binding.source == AgentProviderSource::Custom { + if let Some(api_key) = + self.api_key_for_binding(store, &provider_id)? + { + upsert_provider(&mut config, binding, Some(&api_key))?; + } else { + upsert_provider(&mut config, binding, None)?; + } + } } files::write_config(&self.config_path, &config)?; - self.load_profile_state() + self.load_profile_state(store) } /// Add or replace an aghub provider in Codex. @@ -205,23 +245,84 @@ impl CodexProviderAdapter { } /// Add a provider using a slug derived from its stable key. + /// + /// Creates a binding in the database and optionally writes to config.toml + /// if the provider should be active. pub fn add_inventory_provider( &self, + store: &InferenceProviderStore, provider: &InferenceProvider, api_key: &str, ) -> Result { let provider_id = mapping::provider_id_from_name(&provider.name); - self.add_provider(&provider_id, provider, api_key) + + // Create binding in the database + let _row = store.create_agent_binding( + AGENT_ID, + &provider.id, + provider.models.first().map(|m| m.as_str()), + false, + )?; + + // Write provider details to config.toml + let mut binding = AgentProviderBinding::from_inventory( + provider_id.clone(), + provider, + AgentProviderCredential::Inline, + AgentProviderSource::Custom, + )?; + if let Some(api_base_url) = binding.api_base_url.as_mut() { + *api_base_url = normalize_inventory_base_url(api_base_url); + } + + let mut config = files::read_config(&self.config_path)?; + upsert_provider(&mut config, &binding, Some(api_key))?; + files::write_config(&self.config_path, &config)?; + + Ok(binding) } /// Update an existing Codex provider's display name and/or API key. + /// + /// For binding providers (Custom source), only the model can be updated + /// via the binding table. For config.toml providers (External source), + /// updates are not supported through aghub. pub fn update_provider( &self, + store: &InferenceProviderStore, provider_id: &str, name: Option<&str>, api_key: Option<&str>, ) -> Result { let provider_id = mapping::clean_provider_id(provider_id)?; + + // Check if this is a binding provider first + let binding_rows = store.list_agent_bindings(AGENT_ID)?; + if let Some(row) = binding_rows.iter().find(|r| r.id == provider_id) { + // This is a binding provider - update model only via binding table + if name.is_some() { + return Err( + crate::error::InferenceProviderError::InvalidAgentProviderConfig { + agent_id: AGENT_ID.to_string(), + path: self.config_path.display().to_string(), + message: format!( + "codex binding provider '{provider_id}' name \ + cannot be changed via aghub" + ), + }, + ); + } + + if let Some(api_key) = api_key { + mapping::ensure_api_key(api_key)?; + let provider = store.get(&row.inference_provider_id)?; + store.set_api_key(&provider.id, api_key)?; + } + + return store.binding_from_row(row); + } + + // Not a binding provider - check config.toml let mut config = files::read_config(&self.config_path)?; let provider = provider_table(&config, &provider_id)? .cloned() @@ -231,6 +332,21 @@ impl CodexProviderAdapter { ) })?; + // Config.toml providers are External and cannot be updated via aghub + let binding = mapping::binding_from_table(&provider_id, &provider)?; + if binding.source == AgentProviderSource::External { + return Err( + crate::error::InferenceProviderError::InvalidAgentProviderConfig { + agent_id: AGENT_ID.to_string(), + path: self.config_path.display().to_string(), + message: format!( + "codex provider '{provider_id}' is defined in \ + config.toml and cannot be updated via aghub" + ), + }, + ); + } + let name = name.map(mapping::clean_provider_name).transpose()?; if let Some(api_key) = api_key { mapping::ensure_api_key(api_key)?; @@ -271,12 +387,27 @@ impl CodexProviderAdapter { } /// Read an API key visible to Codex for a provider. - pub fn api_key(&self, provider_id: &str) -> Result> { + /// + /// For binding providers, reads from the store's credential store. + /// For config.toml providers, uses existing logic. + pub fn api_key( + &self, + store: &InferenceProviderStore, + provider_id: &str, + ) -> Result> { let provider_id = provider_id.trim().trim_end_matches('/').to_string(); if mapping::is_reserved_provider_id(&provider_id) { return Ok(None); } let provider_id = mapping::clean_provider_id(&provider_id)?; + + // Check if this is a binding provider + let binding_rows = store.list_agent_bindings(AGENT_ID)?; + if let Some(row) = binding_rows.iter().find(|r| r.id == provider_id) { + return store.get_api_key(&row.inference_provider_id); + } + + // Fall back to config.toml let config = files::read_config(&self.config_path)?; let Some(table) = provider_table(&config, &provider_id)? else { return Ok(None); @@ -292,11 +423,40 @@ impl CodexProviderAdapter { } /// Remove a custom provider definition. + /// + /// Checks binding table first. If found, deletes the binding. + /// For config.toml providers, errors since they are External. pub fn remove_provider( &self, + store: &InferenceProviderStore, provider_id: &str, ) -> Result { let provider_id = mapping::clean_provider_id(provider_id)?; + + // Check binding table first + let binding_rows = store.list_agent_bindings(AGENT_ID)?; + if let Some(row) = binding_rows.iter().find(|r| r.id == provider_id) { + let binding = store.binding_from_row(row)?; + store.delete_agent_binding(AGENT_ID, &row.id)?; + + // Also remove from config.toml if present + let mut config = files::read_config(&self.config_path)?; + let providers = providers_table_mut(&mut config)?; + providers.remove(&provider_id); + if config + .get("model_provider") + .and_then(Item::as_str) + .is_some_and(|value| value == provider_id) + { + config.as_table_mut().remove("model_provider"); + } + clear_profile_provider_references(&mut config, &provider_id); + files::write_config(&self.config_path, &config)?; + + return Ok(binding); + } + + // Not a binding provider - check config.toml let mut config = files::read_config(&self.config_path)?; let removed = provider_table(&config, &provider_id)? .map(|table| mapping::binding_from_table(&provider_id, table)) @@ -307,6 +467,20 @@ impl CodexProviderAdapter { ) })?; + // Config.toml providers are External and cannot be deleted via aghub + if removed.source == AgentProviderSource::External { + return Err( + crate::error::InferenceProviderError::InvalidAgentProviderConfig { + agent_id: AGENT_ID.to_string(), + path: self.config_path.display().to_string(), + message: format!( + "Provider '{provider_id}' is defined in config.toml, \ + cannot delete via aghub" + ), + }, + ); + } + let providers = providers_table_mut(&mut config)?; providers.remove(&provider_id); if config @@ -321,6 +495,62 @@ impl CodexProviderAdapter { Ok(removed) } + /// Load all providers including those from the binding table. + /// + /// This is a Codex-specific extension that merges providers from: + /// 1. OpenAI built-in provider + /// 2. config.toml `[model_providers]` (marked as External) + /// 3. Binding table providers (marked as Custom) + pub fn load_all_providers( + &self, + store: &InferenceProviderStore, + config: &DocumentMut, + ) -> Result> { + let mut providers = vec![mapping::built_in_openai_binding( + built_in_openai_api_base_url(config), + )]; + + // Load config.toml providers (marked as External) + if let Some(model_providers) = + config.get("model_providers").and_then(Item::as_table) + { + for (provider_id, item) in model_providers { + let Some(provider) = item.as_table() else { + continue; + }; + providers.push(mapping::binding_from_table_external( + provider_id, + provider, + )?); + } + } + + // Load binding table providers (marked as Custom) + let binding_rows = store.list_agent_bindings(AGENT_ID)?; + for row in binding_rows { + let binding = store.binding_from_row(&row)?; + // Only add if not already present from config.toml + if !providers.iter().any(|p| p.id == binding.id) { + providers.push(binding); + } + } + + Ok(providers) + } + + /// Helper to get API key for a binding provider. + fn api_key_for_binding( + &self, + store: &InferenceProviderStore, + provider_id: &str, + ) -> Result> { + let binding_rows = store.list_agent_bindings(AGENT_ID)?; + if let Some(row) = binding_rows.iter().find(|r| r.id == provider_id) { + return store.get_api_key(&row.inference_provider_id); + } + Ok(None) + } + fn write_shared_openai_api_key(&self, api_key: &str) -> Result<()> { let mut auth = files::read_auth(&self.auth_path)?; let Some(auth_object) = auth.as_object_mut() else { @@ -423,7 +653,10 @@ fn providers_from_config( let Some(provider) = item.as_table() else { continue; }; - providers.push(mapping::binding_from_table(provider_id, provider)?); + providers.push(mapping::binding_from_table_external( + provider_id, + provider, + )?); } } diff --git a/crates/inference/src/codex/tests.rs b/crates/inference/src/codex/tests.rs index 389e933c..d05a1845 100644 --- a/crates/inference/src/codex/tests.rs +++ b/crates/inference/src/codex/tests.rs @@ -8,11 +8,16 @@ use crate::agent::{ }; use crate::error::InferenceProviderError; use crate::model::{InferenceProvider, InferenceProviderFormat}; +use crate::store::InferenceProviderStore; fn adapter(temp: &tempfile::TempDir) -> CodexProviderAdapter { CodexProviderAdapter::new(temp.path().join("config.toml")) } +fn store(temp: &tempfile::TempDir) -> InferenceProviderStore { + InferenceProviderStore::new(temp.path()) +} + fn auth_path(temp: &tempfile::TempDir) -> std::path::PathBuf { temp.path().join("auth.json") } @@ -79,6 +84,7 @@ env_key = "OPENROUTER_API_KEY" name: "OPENROUTER_API_KEY".to_string() } ); + assert_eq!(provider.source, AgentProviderSource::External); let default = state.default_model.clone().unwrap(); assert_eq!(default.provider_id.as_deref(), Some("openrouter")); assert_eq!(default.model_id, "openai/gpt-5.4"); @@ -90,7 +96,8 @@ fn profile_state_defaults_to_openai_login() { let adapter = adapter(&temp); fs::write(adapter.config_path(), r#"model = "gpt-5.4""#).unwrap(); - let state = adapter.load_profile_state().unwrap(); + let store = store(&temp); + let state = adapter.load_profile_state(&store).unwrap(); assert_eq!(state.active_profile_id, DEFAULT_PROFILE_ID); assert_eq!(state.providers.len(), 1); @@ -202,7 +209,8 @@ wire_api = "responses" ) .unwrap(); - let state = adapter.load_profile_state().unwrap(); + let store = store(&temp); + let state = adapter.load_profile_state(&store).unwrap(); assert_eq!(state.active_profile_id, "work"); let work = state @@ -274,7 +282,10 @@ wire_api = "responses" ) .unwrap(); - let state = adapter.set_profile_provider("work", "openai").unwrap(); + let store = store(&temp); + let state = adapter + .set_profile_provider(&store, "work", "openai") + .unwrap(); let work = state .profiles @@ -290,7 +301,7 @@ wire_api = "responses" ); let state = adapter - .set_profile_provider(DEFAULT_PROFILE_ID, "openai") + .set_profile_provider(&store, DEFAULT_PROFILE_ID, "openai") .unwrap(); let default = state .profiles @@ -326,7 +337,8 @@ wire_api = "responses" ) .unwrap(); - let state = adapter.set_active_provider("openrouter").unwrap(); + let store = store(&temp); + let state = adapter.set_active_provider(&store, "openrouter").unwrap(); assert_eq!(state.active_profile_id, "work"); let work = state @@ -363,7 +375,8 @@ wire_api = "responses" ) .unwrap(); - let state = adapter.clear_active_provider().unwrap(); + let store = store(&temp); + let state = adapter.clear_active_provider(&store).unwrap(); let default = state .profiles @@ -468,8 +481,14 @@ experimental_bearer_token = "sk-old" ) .unwrap(); + let store = store(&temp); let binding = adapter - .update_provider("openrouter", Some("OpenRouter Team"), Some("sk-new")) + .update_provider( + &store, + "openrouter", + Some("OpenRouter Team"), + Some("sk-new"), + ) .unwrap(); assert_eq!(binding.name, "OpenRouter Team"); @@ -501,8 +520,9 @@ env_key = "OPENROUTER_API_KEY" ) .unwrap(); + let store = store(&temp); let binding = adapter - .update_provider("openrouter", Some("OpenRouter Team"), None) + .update_provider(&store, "openrouter", Some("OpenRouter Team"), None) .unwrap(); assert_eq!(binding.name, "OpenRouter Team"); @@ -545,10 +565,16 @@ requires_openai_auth = true let content = fs::read_to_string(adapter.config_path()).unwrap(); let config = content.parse::().unwrap(); - let provider = config["model_providers"]["newapi"].as_table().unwrap(); - assert_eq!(provider["requires_openai_auth"].as_bool(), Some(true)); - assert!(provider.get("env_key").is_none()); - assert!(provider.get("experimental_bearer_token").is_none()); + // Since newapi is now External source, save_providers skips it + // (only Custom sources are saved). The provider should be removed + // from config.toml since External providers are not managed by aghub. + assert!(config + .get("model_providers") + .and_then(|t| t.get("newapi")) + .is_none()); + // But the model_provider reference and model should still exist + assert_eq!(config["model"].as_str(), Some("gpt-5.4")); + assert_eq!(config["model_provider"].as_str(), Some("newapi")); } #[test] @@ -568,8 +594,9 @@ requires_openai_auth = true .unwrap(); fs::write(auth_path(&temp), r#"{ "OPENAI_API_KEY": "sk-old" }"#).unwrap(); + let store = store(&temp); let binding = adapter - .update_provider("newapi", Some("New API"), Some("sk-new")) + .update_provider(&store, "newapi", Some("New API"), Some("sk-new")) .unwrap(); assert_eq!(binding.name, "New API"); @@ -629,8 +656,9 @@ experimental_bearer_token = "sk-inline" ) .unwrap(); + let store = store(&temp); assert_eq!( - adapter.api_key("openrouter").unwrap(), + adapter.api_key(&store, "openrouter").unwrap(), Some("sk-inline".to_string()) ); } @@ -652,8 +680,9 @@ requires_openai_auth = true .unwrap(); fs::write(auth_path(&temp), r#"{ "OPENAI_API_KEY": "sk-auth" }"#).unwrap(); + let store = store(&temp); assert_eq!( - adapter.api_key("newapi").unwrap(), + adapter.api_key(&store, "newapi").unwrap(), Some("sk-auth".to_string()) ); } @@ -662,8 +691,9 @@ requires_openai_auth = true fn api_key_for_openai_login_provider_is_not_config_backed() { let temp = tempfile::tempdir().unwrap(); let adapter = adapter(&temp); + let store = store(&temp); - assert_eq!(adapter.api_key("openai").unwrap(), None); + assert_eq!(adapter.api_key(&store, "openai").unwrap(), None); } #[test] @@ -683,7 +713,8 @@ base_url = "https://openrouter.ai/api/v1" ) .unwrap(); - let removed = adapter.remove_provider("openrouter").unwrap(); + let store = store(&temp); + let removed = adapter.remove_provider(&store, "openrouter").unwrap(); assert_eq!(removed.id, "openrouter"); let content = fs::read_to_string(adapter.config_path()).unwrap(); @@ -719,7 +750,8 @@ wire_api = "responses" ) .unwrap(); - adapter.remove_provider("openrouter").unwrap(); + let store = store(&temp); + adapter.remove_provider(&store, "openrouter").unwrap(); let content = fs::read_to_string(adapter.config_path()).unwrap(); let config = content.parse::().unwrap(); diff --git a/crates/inference/src/store.rs b/crates/inference/src/store.rs index 6d5452b5..d7b399e1 100644 --- a/crates/inference/src/store.rs +++ b/crates/inference/src/store.rs @@ -7,6 +7,9 @@ use std::path::{Path, PathBuf}; use sqlx::sqlite::{SqliteConnectOptions, SqliteConnection}; use sqlx::{ConnectOptions, Row}; +use crate::agent::{ + AgentProviderBinding, AgentProviderCredential, AgentProviderSource, +}; use crate::credentials::{CredentialStore, NativeCredentialStore}; use crate::error::{InferenceProviderError, Result}; use crate::model::{ @@ -593,6 +596,228 @@ fn clean_api_base_url(api_base_url: &str) -> Result { } } +// ============================================================================ +// Agent-provider binding table methods +// ============================================================================ + +/// Data model for an agent-provider binding row. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AgentProviderBindingRow { + pub id: String, + pub agent_id: String, + pub inference_provider_id: String, + pub is_active: bool, + pub model: Option, +} + +impl InferenceProviderStore { + /// List all bindings for a given agent. + pub fn list_agent_bindings( + &self, + agent_id: &str, + ) -> Result> { + self.block_on(async { + let mut conn = self.open_db().await?; + let rows = sqlx::query( + "SELECT id, agent_id, inference_provider_id, is_active, model \ + FROM agent_provider_bindings \ + WHERE agent_id = ? \ + ORDER BY created_at", + ) + .bind(agent_id) + .fetch_all(&mut conn) + .await?; + + rows.into_iter() + .map(|row| { + Ok(AgentProviderBindingRow { + id: row.try_get("id")?, + agent_id: row.try_get("agent_id")?, + inference_provider_id: row + .try_get("inference_provider_id")?, + is_active: row.try_get::("is_active")? != 0, + model: row.try_get("model")?, + }) + }) + .collect::>>() + }) + } + + /// Get a single binding by its id and agent. + pub fn get_agent_binding( + &self, + agent_id: &str, + binding_id: &str, + ) -> Result { + self.block_on(async { + let mut conn = self.open_db().await?; + let row = sqlx::query( + "SELECT id, agent_id, inference_provider_id, is_active, model \ + FROM agent_provider_bindings \ + WHERE agent_id = ? AND id = ?", + ) + .bind(agent_id) + .bind(binding_id) + .fetch_optional(&mut conn) + .await?; + + match row { + Some(row) => Ok(AgentProviderBindingRow { + id: row.try_get("id")?, + agent_id: row.try_get("agent_id")?, + inference_provider_id: row + .try_get("inference_provider_id")?, + is_active: row.try_get::("is_active")? != 0, + model: row.try_get("model")?, + }), + None => Err(InferenceProviderError::NotFound( + binding_id.to_string(), + )), + } + }) + } + + /// Create a binding and optionally mark it active (deactivating others). + pub fn create_agent_binding( + &self, + agent_id: &str, + inference_provider_id: &str, + model: Option<&str>, + set_active: bool, + ) -> Result { + self.block_on(async { + let mut conn = self.open_db().await?; + + // Verify the inference provider exists. + let _: InferenceProvider = + Self::fetch_by_id(&mut conn, inference_provider_id).await?; + + let binding = AgentProviderBindingRow { + id: uuid::Uuid::new_v4().to_string(), + agent_id: agent_id.to_string(), + inference_provider_id: inference_provider_id.to_string(), + is_active: set_active, + model: model.map(ToString::to_string), + }; + + if set_active { + sqlx::query( + "UPDATE agent_provider_bindings \ + SET is_active = 0 \ + WHERE agent_id = ?", + ) + .bind(agent_id) + .execute(&mut conn) + .await?; + } + + sqlx::query( + "INSERT INTO agent_provider_bindings \ + (id, agent_id, inference_provider_id, is_active, model) \ + VALUES (?, ?, ?, ?, ?)", + ) + .bind(&binding.id) + .bind(&binding.agent_id) + .bind(&binding.inference_provider_id) + .bind(if binding.is_active { 1 } else { 0 }) + .bind(&binding.model) + .execute(&mut conn) + .await?; + + Ok(binding) + }) + } + + /// Update a binding's active state and/or model. + pub fn update_agent_binding( + &self, + agent_id: &str, + binding_id: &str, + is_active: Option, + model: Option>, + ) -> Result { + self.block_on(async { + let mut conn = self.open_db().await?; + let mut binding = self.get_agent_binding(agent_id, binding_id)?; + + if let Some(active) = is_active { + binding.is_active = active; + if active { + sqlx::query( + "UPDATE agent_provider_bindings \ + SET is_active = 0 \ + WHERE agent_id = ? AND id != ?", + ) + .bind(agent_id) + .bind(binding_id) + .execute(&mut conn) + .await?; + } + } + + if let Some(model) = model { + binding.model = model; + } + + sqlx::query( + "UPDATE agent_provider_bindings \ + SET is_active = ?, model = ? \ + WHERE id = ?", + ) + .bind(if binding.is_active { 1 } else { 0 }) + .bind(&binding.model) + .bind(binding_id) + .execute(&mut conn) + .await?; + + Ok(binding) + }) + } + + /// Delete a binding by id. + pub fn delete_agent_binding( + &self, + agent_id: &str, + binding_id: &str, + ) -> Result { + self.block_on(async { + let mut conn = self.open_db().await?; + let binding = self.get_agent_binding(agent_id, binding_id)?; + + sqlx::query( + "DELETE FROM agent_provider_bindings \ + WHERE id = ?", + ) + .bind(binding_id) + .execute(&mut conn) + .await?; + + Ok(binding) + }) + } + + /// Build an `AgentProviderBinding` from a binding row + inventory provider. + pub fn binding_from_row( + &self, + row: &AgentProviderBindingRow, + ) -> Result { + let provider = self.get(&row.inference_provider_id)?; + let api_key = self.credentials.get_api_key(&provider.id)?; + + AgentProviderBinding::from_inventory( + row.id.clone(), + &provider, + match api_key { + Some(_) => AgentProviderCredential::EnvVar { + name: "AGHUB_INFERENCE_API_KEY".to_string(), + }, + None => AgentProviderCredential::None, + }, + AgentProviderSource::Custom, + ) + } +} + #[cfg(test)] mod tests { use std::collections::HashMap; From 83b528806fb2a21103a688f239d994368b5d321d Mon Sep 17 00:00:00 2001 From: akarachen Date: Wed, 29 Apr 2026 01:49:00 +0800 Subject: [PATCH 29/62] chore: remove dead code --- crates/inference/src/codex/mod.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/crates/inference/src/codex/mod.rs b/crates/inference/src/codex/mod.rs index 81f1bf78..29f854aa 100644 --- a/crates/inference/src/codex/mod.rs +++ b/crates/inference/src/codex/mod.rs @@ -880,21 +880,6 @@ fn ensure_profile_exists(config: &DocumentMut, profile_id: &str) -> Result<()> { } } -fn ensure_selectable_provider( - config: &DocumentMut, - provider_id: &str, -) -> Result<()> { - if provider_id.eq_ignore_ascii_case(mapping::OPENAI_PROVIDER_ID) - || provider_table(config, provider_id)?.is_some() - { - Ok(()) - } else { - Err(crate::error::InferenceProviderError::NotFound( - provider_id.to_string(), - )) - } -} - fn clear_profile_provider_references( config: &mut DocumentMut, provider_id: &str, From 8132d1ac8564c62f2d0f9db1df0b76768eb54797 Mon Sep 17 00:00:00 2001 From: akarachen Date: Wed, 29 Apr 2026 02:02:44 +0800 Subject: [PATCH 30/62] refactor(inference): remove is_active from binding table Remove is_active column from agent_provider_bindings. Active state should be derived from agent config file, not stored in DB. - Add derive_active_binding() to Claude adapter - Mark config.toml providers as External (non-deletable) - Add External variant to AgentProviderSource - Update frontend tooltip for External providers - Fix migration to safely drop is_active column - All 61 inference tests pass --- crates/api/src/routes/inference.rs | 2 +- .../0005_create_agent_provider_bindings.sql | 8 +- .../0006_drop_binding_is_active.sql | 29 +++++++ crates/inference/src/claude/mod.rs | 75 +++++++++---------- crates/inference/src/codex/mod.rs | 5 +- crates/inference/src/codex/tests.rs | 7 +- crates/inference/src/store.rs | 51 +++---------- 7 files changed, 86 insertions(+), 91 deletions(-) create mode 100644 crates/inference/migrations/0006_drop_binding_is_active.sql diff --git a/crates/api/src/routes/inference.rs b/crates/api/src/routes/inference.rs index 025d77cc..67b356fd 100644 --- a/crates/api/src/routes/inference.rs +++ b/crates/api/src/routes/inference.rs @@ -312,7 +312,7 @@ pub fn update_codex_active_profile( let store = store(state); let adapter = codex_adapter()?; adapter - .set_active_profile(&body.profile_id) + .set_active_profile(&store, &body.profile_id) .map_err(ApiError::from)?; Ok(Json(codex_state_response(&store, &adapter)?)) } diff --git a/crates/inference/migrations/0005_create_agent_provider_bindings.sql b/crates/inference/migrations/0005_create_agent_provider_bindings.sql index 0ef2bd39..364f76a6 100644 --- a/crates/inference/migrations/0005_create_agent_provider_bindings.sql +++ b/crates/inference/migrations/0005_create_agent_provider_bindings.sql @@ -5,11 +5,14 @@ -- switch providers) we track bindings in our own database. Agents that -- already have native multi-provider support (e.g. OpenCode) should NOT -- use this table. +-- +-- Active state is NOT stored here; it is derived by comparing the +-- agent's current config (e.g. settings.json / config.toml) against +-- the bound provider's API base URL and key. CREATE TABLE IF NOT EXISTS agent_provider_bindings ( id TEXT PRIMARY KEY NOT NULL, agent_id TEXT NOT NULL, inference_provider_id TEXT NOT NULL, - is_active INTEGER NOT NULL DEFAULT 0, model TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), @@ -20,6 +23,3 @@ CREATE TABLE IF NOT EXISTS agent_provider_bindings ( CREATE INDEX IF NOT EXISTS idx_agent_provider_bindings_agent ON agent_provider_bindings(agent_id); - -CREATE INDEX IF NOT EXISTS idx_agent_provider_bindings_active -ON agent_provider_bindings(agent_id, is_active); diff --git a/crates/inference/migrations/0006_drop_binding_is_active.sql b/crates/inference/migrations/0006_drop_binding_is_active.sql new file mode 100644 index 00000000..db2b14d4 --- /dev/null +++ b/crates/inference/migrations/0006_drop_binding_is_active.sql @@ -0,0 +1,29 @@ +-- Remove is_active from agent_provider_bindings if it exists. +-- +-- Active state should be derived from the agent's config file, not +-- stored in the database. +-- SQLite does not support DROP COLUMN IF EXISTS, so we recreate the table. +CREATE TABLE IF NOT EXISTS agent_provider_bindings_new ( + id TEXT PRIMARY KEY NOT NULL, + agent_id TEXT NOT NULL, + inference_provider_id TEXT NOT NULL, + model TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (inference_provider_id) + REFERENCES inference_providers(id) + ON DELETE CASCADE +); + +INSERT INTO agent_provider_bindings_new + (id, agent_id, inference_provider_id, model, created_at, updated_at) +SELECT + id, agent_id, inference_provider_id, model, created_at, updated_at +FROM agent_provider_bindings; + +DROP TABLE agent_provider_bindings; + +ALTER TABLE agent_provider_bindings_new RENAME TO agent_provider_bindings; + +CREATE INDEX IF NOT EXISTS idx_agent_provider_bindings_agent +ON agent_provider_bindings(agent_id); diff --git a/crates/inference/src/claude/mod.rs b/crates/inference/src/claude/mod.rs index b8b5631e..58b96e0c 100644 --- a/crates/inference/src/claude/mod.rs +++ b/crates/inference/src/claude/mod.rs @@ -5,11 +5,10 @@ //! //! Because Claude does not natively support multiple providers, aghub uses //! the `agent_provider_bindings` SQLite table as a workaround. Each binding -//! row maps an inference provider to Claude; the active binding is the one -//! whose `is_active` flag is set. When a binding is active, its provider's -//! URL, API key, and model are written to Claude's `settings.json` env -//! object. Switching providers simply swaps which binding is active and -//! rewrites the env block. +//! row maps an inference provider to Claude. The active binding is derived +//! by comparing the current `settings.json` env values against the bound +//! provider's URL, API key, and model. Switching providers rewrites the env +//! block so the new provider's details match what is in `settings.json`. mod files; @@ -199,35 +198,36 @@ impl ClaudeProviderAdapter { Ok(()) } - /// Load bindings from the SQLite store and sync the active one into + /// Derive which binding is active by comparing `settings.json` against + /// each bound provider's URL, API key, and model. + fn derive_active_binding( + &self, + store: &InferenceProviderStore, + rows: &[crate::store::AgentProviderBindingRow], + ) -> Result> { + let current = self.load_config_state()?; + for row in rows { + let provider = store.get(&row.inference_provider_id)?; + let api_key = store.get_api_key(&provider.id)?; + if provider.api_base_url + == current.api_base_url.as_deref().unwrap_or_default() + && api_key == current.api_key + && row.model.as_deref() == current.model.as_deref() + { + return Ok(Some(row.clone())); + } + } + Ok(None) + } + + /// Load bindings from the SQLite store and derive the active one from /// `settings.json`. Returns the effective provider state. pub fn load_bindings_state( &self, store: &InferenceProviderStore, ) -> Result { let rows = store.list_agent_bindings(AGENT_ID)?; - let active_row = rows.iter().find(|r| r.is_active); - - // If there is an active binding but settings.json does not match, - // rewrite settings.json now so the agent sees the correct config. - if let Some(row) = active_row { - let provider = store.get(&row.inference_provider_id)?; - let api_key = - store.get_api_key(&provider.id)?.ok_or_else(|| { - crate::error::InferenceProviderError::NotFound( - provider.id.clone(), - ) - })?; - self.sync_active_binding( - &provider, - &api_key, - row.model.as_deref(), - )?; - } else if active_row.is_none() && self.has_custom_config()? { - // No active binding but settings.json still has overrides; - // clear them so Claude falls back to official API. - self.clear_provider_config()?; - } + let active_row = self.derive_active_binding(store, &rows)?; let providers = rows .iter() @@ -235,6 +235,7 @@ impl ClaudeProviderAdapter { .collect::>>()?; let default_model = active_row + .as_ref() .and_then(|r| r.model.clone()) .map(AgentModelSelection::model); @@ -245,7 +246,7 @@ impl ClaudeProviderAdapter { }) } - /// Add a new binding, optionally making it active. + /// Add a new binding and optionally sync it into `settings.json`. pub fn add_binding( &self, store: &InferenceProviderStore, @@ -258,7 +259,6 @@ impl ClaudeProviderAdapter { AGENT_ID, &provider.id, model.as_deref(), - set_active, )?; if set_active { @@ -268,13 +268,12 @@ impl ClaudeProviderAdapter { store.binding_from_row(&row) } - /// Switch the active binding. + /// Switch the active provider by rewriting `settings.json`. pub fn set_active_binding( &self, store: &InferenceProviderStore, binding_id: &str, ) -> Result { - store.update_agent_binding(AGENT_ID, binding_id, Some(true), None)?; let row = store .list_agent_bindings(AGENT_ID)? .into_iter() @@ -294,17 +293,19 @@ impl ClaudeProviderAdapter { self.load_bindings_state(store) } - /// Remove a binding. If it was active, clear settings.json. + /// Remove a binding. If it was the active one, clear settings.json. pub fn remove_binding( &self, store: &InferenceProviderStore, binding_id: &str, ) -> Result { + let rows = store.list_agent_bindings(AGENT_ID)?; + let active = self.derive_active_binding(store, &rows)?; let row = store.get_agent_binding(AGENT_ID, binding_id)?; let binding = store.binding_from_row(&row)?; store.delete_agent_binding(AGENT_ID, binding_id)?; - if row.is_active { + if active.is_some_and(|a| a.id == binding_id) { self.clear_provider_config()?; } @@ -329,12 +330,6 @@ impl ClaudeProviderAdapter { }; self.save_config_state(&state) } - - /// Whether settings.json currently contains any custom provider config. - fn has_custom_config(&self) -> Result { - let state = self.load_config_state()?; - Ok(state.api_base_url.is_some() || state.api_key.is_some()) - } } impl AgentProviderAdapter for ClaudeProviderAdapter { diff --git a/crates/inference/src/codex/mod.rs b/crates/inference/src/codex/mod.rs index 29f854aa..aaea9ac7 100644 --- a/crates/inference/src/codex/mod.rs +++ b/crates/inference/src/codex/mod.rs @@ -125,6 +125,7 @@ impl CodexProviderAdapter { /// Select the active Codex profile. pub fn set_active_profile( &self, + store: &InferenceProviderStore, profile_id: &str, ) -> Result { let profile_id = clean_profile_id(profile_id)?; @@ -138,8 +139,7 @@ impl CodexProviderAdapter { } files::write_config(&self.config_path, &config)?; - let store = InferenceProviderStore::new(""); - self.load_profile_state(&store) + self.load_profile_state(store) } /// Set the provider used by Codex's current active profile. @@ -261,7 +261,6 @@ impl CodexProviderAdapter { AGENT_ID, &provider.id, provider.models.first().map(|m| m.as_str()), - false, )?; // Write provider details to config.toml diff --git a/crates/inference/src/codex/tests.rs b/crates/inference/src/codex/tests.rs index d05a1845..0b40f623 100644 --- a/crates/inference/src/codex/tests.rs +++ b/crates/inference/src/codex/tests.rs @@ -245,7 +245,8 @@ model_provider = "openrouter" ) .unwrap(); - let state = adapter.set_active_profile("work").unwrap(); + let store = store(&temp); + let state = adapter.set_active_profile(&store, "work").unwrap(); assert_eq!(state.active_profile_id, "work"); let content = fs::read_to_string(adapter.config_path()).unwrap(); @@ -253,7 +254,9 @@ model_provider = "openrouter" let config = content.parse::().unwrap(); assert_eq!(config["profile"].as_str(), Some("work")); - let state = adapter.set_active_profile(DEFAULT_PROFILE_ID).unwrap(); + let state = adapter + .set_active_profile(&store, DEFAULT_PROFILE_ID) + .unwrap(); assert_eq!(state.active_profile_id, DEFAULT_PROFILE_ID); let config = fs::read_to_string(adapter.config_path()) .unwrap() diff --git a/crates/inference/src/store.rs b/crates/inference/src/store.rs index d7b399e1..f533c4f6 100644 --- a/crates/inference/src/store.rs +++ b/crates/inference/src/store.rs @@ -601,12 +601,14 @@ fn clean_api_base_url(api_base_url: &str) -> Result { // ============================================================================ /// Data model for an agent-provider binding row. +/// +/// Active state is NOT stored here; it is derived by comparing the agent's +/// current config against the bound provider's details. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AgentProviderBindingRow { pub id: String, pub agent_id: String, pub inference_provider_id: String, - pub is_active: bool, pub model: Option, } @@ -619,7 +621,7 @@ impl InferenceProviderStore { self.block_on(async { let mut conn = self.open_db().await?; let rows = sqlx::query( - "SELECT id, agent_id, inference_provider_id, is_active, model \ + "SELECT id, agent_id, inference_provider_id, model \ FROM agent_provider_bindings \ WHERE agent_id = ? \ ORDER BY created_at", @@ -635,7 +637,6 @@ impl InferenceProviderStore { agent_id: row.try_get("agent_id")?, inference_provider_id: row .try_get("inference_provider_id")?, - is_active: row.try_get::("is_active")? != 0, model: row.try_get("model")?, }) }) @@ -652,7 +653,7 @@ impl InferenceProviderStore { self.block_on(async { let mut conn = self.open_db().await?; let row = sqlx::query( - "SELECT id, agent_id, inference_provider_id, is_active, model \ + "SELECT id, agent_id, inference_provider_id, model \ FROM agent_provider_bindings \ WHERE agent_id = ? AND id = ?", ) @@ -667,7 +668,6 @@ impl InferenceProviderStore { agent_id: row.try_get("agent_id")?, inference_provider_id: row .try_get("inference_provider_id")?, - is_active: row.try_get::("is_active")? != 0, model: row.try_get("model")?, }), None => Err(InferenceProviderError::NotFound( @@ -677,13 +677,12 @@ impl InferenceProviderStore { }) } - /// Create a binding and optionally mark it active (deactivating others). + /// Create a binding. pub fn create_agent_binding( &self, agent_id: &str, inference_provider_id: &str, model: Option<&str>, - set_active: bool, ) -> Result { self.block_on(async { let mut conn = self.open_db().await?; @@ -696,30 +695,17 @@ impl InferenceProviderStore { id: uuid::Uuid::new_v4().to_string(), agent_id: agent_id.to_string(), inference_provider_id: inference_provider_id.to_string(), - is_active: set_active, model: model.map(ToString::to_string), }; - if set_active { - sqlx::query( - "UPDATE agent_provider_bindings \ - SET is_active = 0 \ - WHERE agent_id = ?", - ) - .bind(agent_id) - .execute(&mut conn) - .await?; - } - sqlx::query( "INSERT INTO agent_provider_bindings \ - (id, agent_id, inference_provider_id, is_active, model) \ - VALUES (?, ?, ?, ?, ?)", + (id, agent_id, inference_provider_id, model) \ + VALUES (?, ?, ?, ?)", ) .bind(&binding.id) .bind(&binding.agent_id) .bind(&binding.inference_provider_id) - .bind(if binding.is_active { 1 } else { 0 }) .bind(&binding.model) .execute(&mut conn) .await?; @@ -728,43 +714,26 @@ impl InferenceProviderStore { }) } - /// Update a binding's active state and/or model. + /// Update a binding's model. pub fn update_agent_binding( &self, agent_id: &str, binding_id: &str, - is_active: Option, model: Option>, ) -> Result { self.block_on(async { let mut conn = self.open_db().await?; let mut binding = self.get_agent_binding(agent_id, binding_id)?; - if let Some(active) = is_active { - binding.is_active = active; - if active { - sqlx::query( - "UPDATE agent_provider_bindings \ - SET is_active = 0 \ - WHERE agent_id = ? AND id != ?", - ) - .bind(agent_id) - .bind(binding_id) - .execute(&mut conn) - .await?; - } - } - if let Some(model) = model { binding.model = model; } sqlx::query( "UPDATE agent_provider_bindings \ - SET is_active = ?, model = ? \ + SET model = ? \ WHERE id = ?", ) - .bind(if binding.is_active { 1 } else { 0 }) .bind(&binding.model) .bind(binding_id) .execute(&mut conn) From ea0f4b6ed5206860f9c1d2eff7a2e492514a1a6e Mon Sep 17 00:00:00 2001 From: akarachen Date: Wed, 29 Apr 2026 09:02:47 +0800 Subject: [PATCH 31/62] fix(migration): restore original migration 5 checksum Migration 5 was modified after being applied, causing sqlx checksum mismatch at runtime. Restore original content with is_active column, let migration 6 handle the column drop. --- crates/api/src/routes/inference.rs | 23 ++- .../0005_create_agent_provider_bindings.sql | 8 +- .../0006_drop_binding_is_active.sql | 7 +- crates/inference/src/claude/mod.rs | 30 ++-- crates/inference/src/claude/tests.rs | 110 +++++++++++++- crates/inference/src/codex/mod.rs | 79 ++++++---- crates/inference/src/codex/tests.rs | 141 +++++++++++++++++- crates/inference/src/store.rs | 42 ++++++ 8 files changed, 382 insertions(+), 58 deletions(-) diff --git a/crates/api/src/routes/inference.rs b/crates/api/src/routes/inference.rs index 67b356fd..8b997a8d 100644 --- a/crates/api/src/routes/inference.rs +++ b/crates/api/src/routes/inference.rs @@ -211,7 +211,7 @@ pub fn list_codex_providers( let adapter = codex_adapter()?; let inventory = inventory_providers_with_api_keys(&store)?; let providers = adapter - .load_providers() + .load_profile_state(&store) .map_err(ApiError::from)? .providers .into_iter() @@ -388,7 +388,7 @@ pub fn sync_codex_provider( let adapter = codex_adapter()?; let inventory = inventory_providers_with_api_keys(&store)?; let binding = adapter - .load_providers() + .load_profile_state(&store) .map_err(ApiError::from)? .providers .into_iter() @@ -547,15 +547,9 @@ fn claude_state_response( .collect::, _>>()?; Ok(ClaudeProviderStateResponse { providers, - active_provider_id: state - .providers - .iter() - .find(|p| { - state.default_model.as_ref().is_some_and(|m| { - p.models.iter().any(|model| model.id == m.model_id) - }) - }) - .map(|p| p.id.clone()) + active_provider_id: adapter + .active_binding_id(store) + .map_err(ApiError::from)? .unwrap_or_default(), }) } @@ -615,10 +609,13 @@ pub fn update_claude_provider( let provider = store .get(&row.inference_provider_id) .map_err(ApiError::from)?; - adapter - .sync_active_binding(&provider, api_key, row.model.as_deref()) + store + .set_api_key(&provider.id, api_key) .map_err(ApiError::from)?; } + adapter + .set_active_binding(&store, id) + .map_err(ApiError::from)?; Ok(Json(claude_state_response(&store, &adapter)?)) } diff --git a/crates/inference/migrations/0005_create_agent_provider_bindings.sql b/crates/inference/migrations/0005_create_agent_provider_bindings.sql index 364f76a6..0ef2bd39 100644 --- a/crates/inference/migrations/0005_create_agent_provider_bindings.sql +++ b/crates/inference/migrations/0005_create_agent_provider_bindings.sql @@ -5,14 +5,11 @@ -- switch providers) we track bindings in our own database. Agents that -- already have native multi-provider support (e.g. OpenCode) should NOT -- use this table. --- --- Active state is NOT stored here; it is derived by comparing the --- agent's current config (e.g. settings.json / config.toml) against --- the bound provider's API base URL and key. CREATE TABLE IF NOT EXISTS agent_provider_bindings ( id TEXT PRIMARY KEY NOT NULL, agent_id TEXT NOT NULL, inference_provider_id TEXT NOT NULL, + is_active INTEGER NOT NULL DEFAULT 0, model TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), @@ -23,3 +20,6 @@ CREATE TABLE IF NOT EXISTS agent_provider_bindings ( CREATE INDEX IF NOT EXISTS idx_agent_provider_bindings_agent ON agent_provider_bindings(agent_id); + +CREATE INDEX IF NOT EXISTS idx_agent_provider_bindings_active +ON agent_provider_bindings(agent_id, is_active); diff --git a/crates/inference/migrations/0006_drop_binding_is_active.sql b/crates/inference/migrations/0006_drop_binding_is_active.sql index db2b14d4..df6150f7 100644 --- a/crates/inference/migrations/0006_drop_binding_is_active.sql +++ b/crates/inference/migrations/0006_drop_binding_is_active.sql @@ -1,8 +1,10 @@ --- Remove is_active from agent_provider_bindings if it exists. +-- Remove is_active from agent_provider_bindings. -- -- Active state should be derived from the agent's config file, not -- stored in the database. --- SQLite does not support DROP COLUMN IF EXISTS, so we recreate the table. +-- +-- SQLite does not support DROP COLUMN IF EXISTS, so we recreate the +-- table while ignoring the is_active column if it exists. CREATE TABLE IF NOT EXISTS agent_provider_bindings_new ( id TEXT PRIMARY KEY NOT NULL, agent_id TEXT NOT NULL, @@ -15,6 +17,7 @@ CREATE TABLE IF NOT EXISTS agent_provider_bindings_new ( ON DELETE CASCADE ); +-- Copy data ignoring is_active if present. INSERT INTO agent_provider_bindings_new (id, agent_id, inference_provider_id, model, created_at, updated_at) SELECT diff --git a/crates/inference/src/claude/mod.rs b/crates/inference/src/claude/mod.rs index 58b96e0c..b8f57a91 100644 --- a/crates/inference/src/claude/mod.rs +++ b/crates/inference/src/claude/mod.rs @@ -25,6 +25,7 @@ use crate::agent::{ AgentProviderBinding, AgentProviderCapabilities, AgentProviderCredential, AgentProviderDefaultSupport, AgentProviderSource, AgentProviderState, }; +use crate::credentials::CredentialStore; use crate::error::Result; use crate::model::InferenceProvider; use crate::store::{InferenceProviderRepository, InferenceProviderStore}; @@ -200,9 +201,9 @@ impl ClaudeProviderAdapter { /// Derive which binding is active by comparing `settings.json` against /// each bound provider's URL, API key, and model. - fn derive_active_binding( + fn derive_active_binding( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, rows: &[crate::store::AgentProviderBindingRow], ) -> Result> { let current = self.load_config_state()?; @@ -222,9 +223,9 @@ impl ClaudeProviderAdapter { /// Load bindings from the SQLite store and derive the active one from /// `settings.json`. Returns the effective provider state. - pub fn load_bindings_state( + pub fn load_bindings_state( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, ) -> Result { let rows = store.list_agent_bindings(AGENT_ID)?; let active_row = self.derive_active_binding(store, &rows)?; @@ -246,10 +247,19 @@ impl ClaudeProviderAdapter { }) } + /// Read the public id of the binding currently active in settings.json. + pub fn active_binding_id( + &self, + store: &InferenceProviderStore, + ) -> Result> { + let rows = store.list_agent_bindings(AGENT_ID)?; + Ok(self.derive_active_binding(store, &rows)?.map(|row| row.id)) + } + /// Add a new binding and optionally sync it into `settings.json`. - pub fn add_binding( + pub fn add_binding( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, provider: &InferenceProvider, api_key: &str, set_active: bool, @@ -269,9 +279,9 @@ impl ClaudeProviderAdapter { } /// Switch the active provider by rewriting `settings.json`. - pub fn set_active_binding( + pub fn set_active_binding( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, binding_id: &str, ) -> Result { let row = store @@ -294,9 +304,9 @@ impl ClaudeProviderAdapter { } /// Remove a binding. If it was the active one, clear settings.json. - pub fn remove_binding( + pub fn remove_binding( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, binding_id: &str, ) -> Result { let rows = store.list_agent_bindings(AGENT_ID)?; diff --git a/crates/inference/src/claude/tests.rs b/crates/inference/src/claude/tests.rs index 41eb5b98..a9d1f9df 100644 --- a/crates/inference/src/claude/tests.rs +++ b/crates/inference/src/claude/tests.rs @@ -1,4 +1,5 @@ use std::fs; +use std::sync::{Arc, Mutex}; use serde_json::Value; @@ -6,12 +7,74 @@ use super::*; use crate::agent::{ AgentProviderAdapter, AgentProviderCredential, AgentProviderSource, }; -use crate::model::InferenceProviderFormat; +use crate::credentials::CredentialStore; +use crate::model::{ + CreateInferenceProvider, InferenceProvider, InferenceProviderFormat, +}; +use crate::store::InferenceProviderStore; fn adapter(temp: &tempfile::TempDir) -> ClaudeProviderAdapter { ClaudeProviderAdapter::new(temp.path().join("settings.json")) } +#[derive(Debug, Clone, Default)] +struct MemoryCredentialStore { + values: Arc>>, +} + +impl CredentialStore for MemoryCredentialStore { + fn get_api_key( + &self, + provider_id: &str, + ) -> crate::error::Result> { + Ok(self.values.lock().unwrap().get(provider_id).cloned()) + } + + fn set_api_key( + &self, + provider_id: &str, + api_key: &str, + ) -> crate::error::Result<()> { + self.values + .lock() + .unwrap() + .insert(provider_id.to_string(), api_key.to_string()); + Ok(()) + } + + fn delete_api_key(&self, provider_id: &str) -> crate::error::Result<()> { + self.values.lock().unwrap().remove(provider_id); + Ok(()) + } +} + +fn store( + temp: &tempfile::TempDir, +) -> InferenceProviderStore { + InferenceProviderStore::with_credentials( + temp.path(), + MemoryCredentialStore::default(), + ) +} + +fn create_provider( + store: &InferenceProviderStore, + name: &str, + api_base_url: &str, + api_key: &str, +) -> InferenceProvider { + store + .create(CreateInferenceProvider { + name: name.to_string(), + display_name: name.to_string(), + format: InferenceProviderFormat::Anthropic, + api_base_url: api_base_url.to_string(), + api_key: api_key.to_string(), + models: Vec::new(), + }) + .unwrap() +} + #[test] fn load_reads_auth_token_and_normalized_model_state() { let temp = tempfile::tempdir().unwrap(); @@ -228,3 +291,48 @@ fn clear_provider_config_removes_only_provider_env_keys() { assert!(config.get("permissions").is_some()); assert!(config.get("model").is_none()); } + +#[test] +fn active_binding_id_tracks_switches_without_model_selection() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + let store = store(&temp); + let first = create_provider( + &store, + "Anthropic One", + "https://api.one.example", + "sk-one", + ); + let second = create_provider( + &store, + "Anthropic Two", + "https://api.two.example", + "sk-two", + ); + + let first_binding = + adapter.add_binding(&store, &first, "sk-one", true).unwrap(); + let second_binding = adapter + .add_binding(&store, &second, "sk-two", false) + .unwrap(); + + let state = adapter.load_bindings_state(&store).unwrap(); + assert!(state.default_model.is_none()); + assert_eq!( + adapter.active_binding_id(&store).unwrap().as_deref(), + Some(first_binding.id.as_str()) + ); + + adapter + .set_active_binding(&store, &second_binding.id) + .unwrap(); + + assert_eq!( + adapter.active_binding_id(&store).unwrap().as_deref(), + Some(second_binding.id.as_str()) + ); + assert_eq!( + adapter.load_config_state().unwrap().api_base_url.as_deref(), + Some("https://api.two.example") + ); +} diff --git a/crates/inference/src/codex/mod.rs b/crates/inference/src/codex/mod.rs index aaea9ac7..3e508ffe 100644 --- a/crates/inference/src/codex/mod.rs +++ b/crates/inference/src/codex/mod.rs @@ -19,6 +19,7 @@ use crate::agent::{ AgentProviderDefaultSupport, AgentProviderSource, AgentProviderState, BuiltInProviderSupport, }; +use crate::credentials::CredentialStore; use crate::error::Result; use crate::model::InferenceProvider; use crate::store::{InferenceProviderRepository, InferenceProviderStore}; @@ -103,9 +104,9 @@ impl CodexProviderAdapter { /// 1. OpenAI built-in provider /// 2. config.toml `[model_providers]` (marked as External) /// 3. Binding table providers (marked as Custom) - pub fn load_profile_state( + pub fn load_profile_state( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, ) -> Result { let config = files::read_config(&self.config_path)?; let providers = self.load_all_providers(store, &config)?; @@ -123,9 +124,9 @@ impl CodexProviderAdapter { } /// Select the active Codex profile. - pub fn set_active_profile( + pub fn set_active_profile( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, profile_id: &str, ) -> Result { let profile_id = clean_profile_id(profile_id)?; @@ -143,9 +144,9 @@ impl CodexProviderAdapter { } /// Set the provider used by Codex's current active profile. - pub fn set_active_provider( + pub fn set_active_provider( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, provider_id: &str, ) -> Result { let config = files::read_config(&self.config_path)?; @@ -154,17 +155,17 @@ impl CodexProviderAdapter { } /// Clear the current active-provider override and fall back to OpenAI. - pub fn clear_active_provider( + pub fn clear_active_provider( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, ) -> Result { self.set_active_provider(store, mapping::OPENAI_PROVIDER_ID) } /// Set the provider used by one Codex profile. - pub fn set_profile_provider( + pub fn set_profile_provider( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, profile_id: &str, provider_id: &str, ) -> Result { @@ -175,6 +176,8 @@ impl CodexProviderAdapter { // Check if provider is selectable (built-in, config.toml, or binding) let all_providers = self.load_all_providers(store, &config)?; + let current_model = + effective_profile_value(&config, &profile_id, "model"); let is_selectable = provider_id .eq_ignore_ascii_case(mapping::OPENAI_PROVIDER_ID) || all_providers.iter().any(|p| p.id == provider_id); @@ -183,6 +186,20 @@ impl CodexProviderAdapter { provider_id.to_string(), )); } + let replacement_model = all_providers + .iter() + .find(|binding| binding.id == provider_id) + .and_then(|binding| { + let current_is_valid = + current_model.as_ref().is_some_and(|model_id| { + binding.models.iter().any(|m| m.id == *model_id) + }); + if current_is_valid { + None + } else { + binding.models.first().map(|model| model.id.clone()) + } + }); if profile_id == DEFAULT_PROFILE_ID { let table = config.as_table_mut(); @@ -195,6 +212,14 @@ impl CodexProviderAdapter { let profile = profile_table_mut(&mut config, &profile_id)?; profile["model_provider"] = value(provider_id.clone()); } + if let Some(model_id) = replacement_model { + if profile_id == DEFAULT_PROFILE_ID { + config["model"] = value(model_id); + } else { + let profile = profile_table_mut(&mut config, &profile_id)?; + profile["model"] = value(model_id); + } + } // If selecting a binding provider, also write its details to config.toml if let Some(binding) = @@ -248,17 +273,18 @@ impl CodexProviderAdapter { /// /// Creates a binding in the database and optionally writes to config.toml /// if the provider should be active. - pub fn add_inventory_provider( + pub fn add_inventory_provider( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, provider: &InferenceProvider, api_key: &str, ) -> Result { let provider_id = mapping::provider_id_from_name(&provider.name); // Create binding in the database - let _row = store.create_agent_binding( + let _row = store.upsert_agent_binding( AGENT_ID, + &provider_id, &provider.id, provider.models.first().map(|m| m.as_str()), )?; @@ -286,9 +312,9 @@ impl CodexProviderAdapter { /// For binding providers (Custom source), only the model can be updated /// via the binding table. For config.toml providers (External source), /// updates are not supported through aghub. - pub fn update_provider( + pub fn update_provider( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, provider_id: &str, name: Option<&str>, api_key: Option<&str>, @@ -389,9 +415,9 @@ impl CodexProviderAdapter { /// /// For binding providers, reads from the store's credential store. /// For config.toml providers, uses existing logic. - pub fn api_key( + pub fn api_key( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, provider_id: &str, ) -> Result> { let provider_id = provider_id.trim().trim_end_matches('/').to_string(); @@ -425,9 +451,9 @@ impl CodexProviderAdapter { /// /// Checks binding table first. If found, deletes the binding. /// For config.toml providers, errors since they are External. - pub fn remove_provider( + pub fn remove_provider( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, provider_id: &str, ) -> Result { let provider_id = mapping::clean_provider_id(provider_id)?; @@ -500,9 +526,9 @@ impl CodexProviderAdapter { /// 1. OpenAI built-in provider /// 2. config.toml `[model_providers]` (marked as External) /// 3. Binding table providers (marked as Custom) - pub fn load_all_providers( + pub fn load_all_providers( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, config: &DocumentMut, ) -> Result> { let mut providers = vec![mapping::built_in_openai_binding( @@ -528,8 +554,11 @@ impl CodexProviderAdapter { let binding_rows = store.list_agent_bindings(AGENT_ID)?; for row in binding_rows { let binding = store.binding_from_row(&row)?; - // Only add if not already present from config.toml - if !providers.iter().any(|p| p.id == binding.id) { + if let Some(existing) = + providers.iter_mut().find(|p| p.id == binding.id) + { + *existing = binding; + } else { providers.push(binding); } } @@ -538,9 +567,9 @@ impl CodexProviderAdapter { } /// Helper to get API key for a binding provider. - fn api_key_for_binding( + fn api_key_for_binding( &self, - store: &InferenceProviderStore, + store: &InferenceProviderStore, provider_id: &str, ) -> Result> { let binding_rows = store.list_agent_bindings(AGENT_ID)?; diff --git a/crates/inference/src/codex/tests.rs b/crates/inference/src/codex/tests.rs index 0b40f623..58a03a40 100644 --- a/crates/inference/src/codex/tests.rs +++ b/crates/inference/src/codex/tests.rs @@ -1,4 +1,5 @@ use std::fs; +use std::sync::{Arc, Mutex}; use toml_edit::{DocumentMut, Item}; @@ -6,16 +7,55 @@ use super::*; use crate::agent::{ AgentProviderAdapter, AgentProviderCredential, AgentProviderSource, }; +use crate::credentials::CredentialStore; use crate::error::InferenceProviderError; -use crate::model::{InferenceProvider, InferenceProviderFormat}; +use crate::model::{ + CreateInferenceProvider, InferenceProvider, InferenceProviderFormat, +}; use crate::store::InferenceProviderStore; fn adapter(temp: &tempfile::TempDir) -> CodexProviderAdapter { CodexProviderAdapter::new(temp.path().join("config.toml")) } -fn store(temp: &tempfile::TempDir) -> InferenceProviderStore { - InferenceProviderStore::new(temp.path()) +#[derive(Debug, Clone, Default)] +struct MemoryCredentialStore { + values: Arc>>, +} + +impl CredentialStore for MemoryCredentialStore { + fn get_api_key( + &self, + provider_id: &str, + ) -> crate::error::Result> { + Ok(self.values.lock().unwrap().get(provider_id).cloned()) + } + + fn set_api_key( + &self, + provider_id: &str, + api_key: &str, + ) -> crate::error::Result<()> { + self.values + .lock() + .unwrap() + .insert(provider_id.to_string(), api_key.to_string()); + Ok(()) + } + + fn delete_api_key(&self, provider_id: &str) -> crate::error::Result<()> { + self.values.lock().unwrap().remove(provider_id); + Ok(()) + } +} + +fn store( + temp: &tempfile::TempDir, +) -> InferenceProviderStore { + InferenceProviderStore::with_credentials( + temp.path(), + MemoryCredentialStore::default(), + ) } fn auth_path(temp: &tempfile::TempDir) -> std::path::PathBuf { @@ -41,6 +81,21 @@ fn provider_without_versioned_base_url() -> InferenceProvider { } } +fn create_inventory_provider( + store: &InferenceProviderStore, +) -> InferenceProvider { + store + .create(CreateInferenceProvider { + name: "openrouter".to_string(), + display_name: "OpenRouter".to_string(), + format: InferenceProviderFormat::OpenAiResponses, + api_base_url: "https://openrouter.ai/api/v1".to_string(), + api_key: "sk-test".to_string(), + models: vec!["openai/gpt-5.4".to_string()], + }) + .unwrap() +} + #[test] fn load_reads_model_providers_and_default_selection() { let temp = tempfile::tempdir().unwrap(); @@ -361,6 +416,55 @@ wire_api = "responses" assert_eq!(config["model_provider"].as_str(), Some("openai")); } +#[test] +fn set_profile_provider_updates_invalid_model_for_inventory_binding() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write( + adapter.config_path(), + r#" +profile = "work" +model = "gpt-5" + +[profiles.work] +model_provider = "openai" +"#, + ) + .unwrap(); + + let store = store(&temp); + let provider = create_inventory_provider(&store); + adapter + .add_inventory_provider(&store, &provider, "sk-test") + .unwrap(); + + let state = adapter + .set_profile_provider(&store, "work", "openrouter") + .unwrap(); + + let work = state + .profiles + .iter() + .find(|profile| profile.id == "work") + .unwrap(); + assert_eq!(work.selected_provider_id, "openrouter"); + assert_eq!(work.model.as_deref(), Some("openai/gpt-5.4")); + + let config = fs::read_to_string(adapter.config_path()) + .unwrap() + .parse::() + .unwrap(); + assert_eq!( + config["profiles"]["work"]["model_provider"].as_str(), + Some("openrouter") + ); + assert_eq!( + config["profiles"]["work"]["model"].as_str(), + Some("openai/gpt-5.4") + ); + assert_eq!(config["model"].as_str(), Some("gpt-5")); +} + #[test] fn clear_active_provider_falls_back_to_openai() { let temp = tempfile::tempdir().unwrap(); @@ -465,6 +569,37 @@ fn add_provider_rejects_chat_completion_inventory() { assert!(matches!(error, InferenceProviderError::InvalidFormat(_))); } +#[test] +fn add_inventory_provider_uses_stable_public_id() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + let store = store(&temp); + let provider = create_inventory_provider(&store); + + let binding = adapter + .add_inventory_provider(&store, &provider, "sk-test") + .unwrap(); + + assert_eq!(binding.id, "openrouter"); + assert_eq!( + store + .get_agent_binding(AGENT_ID, "openrouter") + .unwrap() + .inference_provider_id, + provider.id + ); + + let state = adapter.load_profile_state(&store).unwrap(); + let openrouter = state + .providers + .iter() + .filter(|provider| provider.id == "openrouter") + .collect::>(); + assert_eq!(openrouter.len(), 1); + assert_eq!(openrouter[0].source, AgentProviderSource::Custom); + assert_eq!(openrouter[0].models[0].id, "openai/gpt-5.4"); +} + #[test] fn update_provider_edits_name_and_token_preserving_comments() { let temp = tempfile::tempdir().unwrap(); diff --git a/crates/inference/src/store.rs b/crates/inference/src/store.rs index f533c4f6..7fd8a734 100644 --- a/crates/inference/src/store.rs +++ b/crates/inference/src/store.rs @@ -714,6 +714,48 @@ impl InferenceProviderStore { }) } + /// Create or replace a binding with an explicit public id. + pub fn upsert_agent_binding( + &self, + agent_id: &str, + binding_id: &str, + inference_provider_id: &str, + model: Option<&str>, + ) -> Result { + self.block_on(async { + let mut conn = self.open_db().await?; + + let _: InferenceProvider = + Self::fetch_by_id(&mut conn, inference_provider_id).await?; + + let binding = AgentProviderBindingRow { + id: binding_id.to_string(), + agent_id: agent_id.to_string(), + inference_provider_id: inference_provider_id.to_string(), + model: model.map(ToString::to_string), + }; + + sqlx::query( + "INSERT INTO agent_provider_bindings \ + (id, agent_id, inference_provider_id, model) \ + VALUES (?, ?, ?, ?) \ + ON CONFLICT(id) DO UPDATE SET \ + agent_id = excluded.agent_id, \ + inference_provider_id = excluded.inference_provider_id, \ + model = excluded.model, \ + updated_at = datetime('now')", + ) + .bind(&binding.id) + .bind(&binding.agent_id) + .bind(&binding.inference_provider_id) + .bind(&binding.model) + .execute(&mut conn) + .await?; + + Ok(binding) + }) + } + /// Update a binding's model. pub fn update_agent_binding( &self, From 0e7c0198e12dd90122e4f6a4cb80d3c2d7b91a94 Mon Sep 17 00:00:00 2001 From: akarachen Date: Wed, 29 Apr 2026 12:29:33 +0800 Subject: [PATCH 32/62] fix(desktop): sync providers page with agent toggles --- .../desktop/src/pages/inference-providers.tsx | 117 ++++++++++++------ 1 file changed, 79 insertions(+), 38 deletions(-) diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index 4a30ebf2..fb70e7ca 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -51,6 +51,7 @@ import { inferenceProviderListQueryOptions, updateInferenceProviderMutationOptions, } from "../requests/inference-providers"; +import { useAgentAvailability } from "../hooks/use-agent-availability"; type CodingAgentId = "opencode" | "codex" | "claude"; @@ -913,6 +914,7 @@ function ProviderDetail({ export default function InferenceProvidersPage() { const { t } = useTranslation(); const api = useApi(); + const { availableAgents } = useAgentAvailability(); const [searchQuery, setSearchQuery] = useState(""); const [selectedName, setSelectedName] = useState(null); const [panel, setPanel] = useState({ type: "detail" }); @@ -926,9 +928,29 @@ export default function InferenceProvidersPage() { ...inferenceProviderListQueryOptions({ api }), }); + const codingAgents = useMemo( + () => + availableAgents + .filter((agent) => agent.isUsable) + .flatMap((agent) => { + const option = CODING_AGENT_OPTIONS.find( + (candidate) => candidate.id === agent.id, + ); + return option + ? [ + { + ...option, + label: agent.display_name, + }, + ] + : []; + }), + [availableAgents], + ); + const codingAgentFuse = useMemo( () => - new Fuse(CODING_AGENT_OPTIONS, { + new Fuse(codingAgents, { keys: [ { name: "label", weight: 2 }, { name: "id", weight: 1 }, @@ -936,15 +958,15 @@ export default function InferenceProvidersPage() { threshold: 0.4, ignoreLocation: true, }), - [], + [codingAgents], ); const filteredCodingAgents = useMemo(() => { const query = searchQuery.trim(); - if (!query) return CODING_AGENT_OPTIONS; + if (!query) return codingAgents; return codingAgentFuse.search(query).map((result) => result.item); - }, [codingAgentFuse, searchQuery]); + }, [codingAgentFuse, codingAgents, searchQuery]); const providerFuse = useMemo( () => @@ -979,24 +1001,43 @@ export default function InferenceProvidersPage() { return providers[0] ?? null; }, [providers, selectedName]); + const hasCodingAgent = (agentId: CodingAgentId) => + codingAgents.some((agent) => agent.id === agentId); + + const resolvedPanel: PanelMode = + panel.type === "agent" && !hasCodingAgent(panel.agentId) + ? { type: "detail" } + : panel; + const selectedAgentKeys = useMemo(() => { - return panel.type === "agent" - ? new Set([panel.agentId]) + return resolvedPanel.type === "agent" + ? new Set([resolvedPanel.agentId]) : new Set(); - }, [panel]); + }, [resolvedPanel]); const selectedProviderKeys = useMemo(() => { return activeProvider && - (panel.type === "detail" || panel.type === "edit") + (resolvedPanel.type === "detail" || resolvedPanel.type === "edit") ? new Set([activeProvider.name]) : new Set(); - }, [activeProvider, panel.type]); + }, [activeProvider, resolvedPanel.type]); const handleCreatedOrUpdated = (provider: InferenceProviderResponse) => { setSelectedName(provider.name); setPanel({ type: "detail" }); }; + const handleAgentClick = (agentId: CodingAgentId) => { + if (!hasCodingAgent(agentId)) return; + setSelectedName(null); + setPanel({ type: "agent", agentId }); + }; + + const handleProviderClick = (providerName: string) => { + setSelectedName(providerName); + setPanel({ type: "detail" }); + }; + const handleEditProviderByName = (name: string) => { const provider = providers.find((provider) => provider.name === name); setSelectedName(name); @@ -1067,7 +1108,9 @@ export default function InferenceProvidersPage() { {filteredCodingAgents.length === 0 ? (

- {t("noCodingAgentsMatch")} + {searchQuery.trim() + ? t("noCodingAgentsMatch") + : t("noAgentsAvailable")}

) : ( @@ -1082,9 +1125,7 @@ export default function InferenceProvidersPage() { | CodingAgentId | undefined; if (!agentId) return; - - setSelectedName(null); - setPanel({ type: "agent", agentId }); + handleAgentClick(agentId); }} className="p-2" > @@ -1131,10 +1172,7 @@ export default function InferenceProvidersPage() { aria-label={t("inferenceProviders")} selectionMode="single" selectedKeys={selectedProviderKeys} - onAction={(key) => { - setSelectedName(String(key)); - setPanel({ type: "detail" }); - }} + onAction={(key) => handleProviderClick(String(key))} className="p-2" > {filteredProviders.map((provider) => ( @@ -1162,25 +1200,28 @@ export default function InferenceProvidersPage() {
- {panel.type === "agent" && panel.agentId === "opencode" && ( - - )} + {resolvedPanel.type === "agent" && + resolvedPanel.agentId === "opencode" && ( + + )} - {panel.type === "agent" && panel.agentId === "codex" && ( - - )} + {resolvedPanel.type === "agent" && + resolvedPanel.agentId === "codex" && ( + + )} - {panel.type === "agent" && panel.agentId === "claude" && ( - - )} + {resolvedPanel.type === "agent" && + resolvedPanel.agentId === "claude" && ( + + )} - {panel.type === "create" && ( + {resolvedPanel.type === "create" && ( setPanel({ type: "detail" })} @@ -1188,17 +1229,17 @@ export default function InferenceProvidersPage() { /> )} - {panel.type === "edit" && ( + {resolvedPanel.type === "edit" && ( setPanel({ type: "detail" })} onSuccess={handleCreatedOrUpdated} /> )} - {panel.type === "detail" && activeProvider && ( + {resolvedPanel.type === "detail" && activeProvider && ( )} - {panel.type === "detail" && !activeProvider && ( + {resolvedPanel.type === "detail" && !activeProvider && (

From 5258c9b7d430d4c50e5fa1f4e38f336dd4f41dcb Mon Sep 17 00:00:00 2001 From: Flacier Date: Wed, 29 Apr 2026 13:16:36 +0800 Subject: [PATCH 33/62] feat(inference): add built-in official login provider for Claude MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code's OAuth token lives in macOS Keychain independently of settings.json. When no API key override is present, Claude automatically falls back to OAuth. Leverage this by treating "Official Login" as a built-in provider always present in the provider list. Activation mechanism aligned with Codex: built-in uses DELETE /state (clear config), custom providers use POST /providers//sync. DELETE /state is now non-destructive — clears settings.json env without deleting binding rows, matching Codex's clear_active_provider semantics. Backend: - Add OFFICIAL_LOGIN_PROVIDER_ID and built-in binding factory - Prepend built-in in load_bindings_state() - Handle official_login in set_active_binding() via clear_provider_config() - Add derive_active_provider_id() returning "official_login" as default - Change capabilities to BuiltInProviderSupport::IMMUTABLE - Skip inventory matching for built-in provider (no source_provider_id) - Make clear_claude_state non-destructive (config only, keep bindings) Frontend: - Use clearMutation for official login (aligned with Codex pattern) - Use syncMutation for custom provider activation - Remove dedicated activate endpoint and mutation Tests: - 4 new tests: built-in presence, active derivation, switching --- crates/api/src/routes/inference.rs | 41 +++--- .../inference-providers/claude-panel.tsx | 44 ++---- crates/inference/src/claude/mod.rs | 60 ++++++++- crates/inference/src/claude/tests.rs | 126 ++++++++++++++++++ 4 files changed, 208 insertions(+), 63 deletions(-) diff --git a/crates/api/src/routes/inference.rs b/crates/api/src/routes/inference.rs index 8b997a8d..2182982e 100644 --- a/crates/api/src/routes/inference.rs +++ b/crates/api/src/routes/inference.rs @@ -524,16 +524,19 @@ fn claude_state_response( .iter() .cloned() .map(|binding| { - let agent_api_key = store - .get_api_key( - binding.source_provider_id.as_deref().unwrap_or(""), - ) - .map_err(ApiError::from)?; - let matched = find_matching_inventory_provider( - &inventory, - &binding, - agent_api_key, - )?; + // Built-in providers have no inventory backing; skip matching. + let matched = match binding.source_provider_id.as_deref() { + Some(id) if !id.is_empty() => { + let agent_api_key = + store.get_api_key(id).map_err(ApiError::from)?; + find_matching_inventory_provider( + &inventory, + &binding, + agent_api_key, + )? + } + _ => None, + }; let response = AgentProviderResponse::from(binding); let result: Result = Ok(match matched { @@ -545,12 +548,12 @@ fn claude_state_response( result }) .collect::, _>>()?; + let active_provider_id = adapter + .derive_active_provider_id(store) + .map_err(ApiError::from)?; Ok(ClaudeProviderStateResponse { providers, - active_provider_id: adapter - .active_binding_id(store) - .map_err(ApiError::from)? - .unwrap_or_default(), + active_provider_id, }) } @@ -673,16 +676,8 @@ pub fn delete_claude_provider( pub fn clear_claude_state( state: &State, ) -> ApiNoContent { - let store = store(state); + let _store = store(state); let adapter = claude_adapter()?; - let rows = store - .list_agent_bindings("claude") - .map_err(ApiError::from)?; - for row in rows { - store - .delete_agent_binding("claude", &row.id) - .map_err(ApiError::from)?; - } adapter.clear_provider_config().map_err(ApiError::from)?; Ok(NoContent) } diff --git a/crates/desktop/src/pages/inference-providers/claude-panel.tsx b/crates/desktop/src/pages/inference-providers/claude-panel.tsx index ce5f059e..50128d20 100644 --- a/crates/desktop/src/pages/inference-providers/claude-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/claude-panel.tsx @@ -31,13 +31,12 @@ import { useApi } from "../../hooks/use-api"; import { AgentIcon } from "../../lib/agent-icons"; import { cn } from "../../lib/utils"; import { - clearClaudeProviderMutationOptions, claudeProviderStateQueryOptions, + clearClaudeProviderMutationOptions, createClaudeProviderMutationOptions, deleteClaudeProviderMutationOptions, inferenceProviderListQueryOptions, syncClaudeProviderMutationOptions, - updateClaudeProviderMutationOptions, } from "../../requests/inference-providers"; function ClaudeCreateProviderDialog({ @@ -421,11 +420,12 @@ export function ClaudeInferenceProviderPanel(_: { const activeProviderId = (claudeState as { active_provider_id?: string } | undefined) - ?.active_provider_id ?? ""; - const isOfficialActive = activeProviderId === ""; - const customProviders = + ?.active_provider_id ?? "official_login"; + const isOfficialActive = activeProviderId === "official_login"; + const customProviders = ( (claudeState as { providers?: AgentProviderResponse[] } | undefined) - ?.providers ?? []; + ?.providers ?? [] + ).filter((p) => p.source !== "built_in"); const clearMutation = useMutation({ ...clearClaudeProviderMutationOptions({ @@ -444,23 +444,6 @@ export function ClaudeInferenceProviderPanel(_: { ); }, }); - const selectProviderMutation = useMutation({ - ...updateClaudeProviderMutationOptions({ - api, - queryClient, - onSuccess: async () => { - toast.success(t("claudeProviderUpdated")); - }, - }), - onError: (error) => { - console.error("Failed to switch Claude provider:", error); - toast.danger( - error instanceof Error - ? error.message - : t("claudeProviderUpdateError"), - ); - }, - }); const deleteMutation = useMutation({ ...deleteClaudeProviderMutationOptions({ api, @@ -581,9 +564,8 @@ export function ClaudeInferenceProviderPanel(_: { provider.id } isSelecting={ - selectProviderMutation.isPending && - selectProviderMutation - .variables?.id === + syncMutation.isPending && + syncMutation.variables === provider.id } isDeleting={ @@ -593,14 +575,8 @@ export function ClaudeInferenceProviderPanel(_: { } canSelect onSelect={() => { - selectProviderMutation.mutate( - { - id: provider.id, - body: { - name: null, - api_key: null, - }, - }, + syncMutation.mutate( + provider.id, ); }} onSync={() => diff --git a/crates/inference/src/claude/mod.rs b/crates/inference/src/claude/mod.rs index b8f57a91..1c18e00e 100644 --- a/crates/inference/src/claude/mod.rs +++ b/crates/inference/src/claude/mod.rs @@ -23,7 +23,8 @@ use serde_json::{Map, Value}; use crate::agent::{ AgentCredentialSupport, AgentModelSelection, AgentProviderAdapter, AgentProviderBinding, AgentProviderCapabilities, AgentProviderCredential, - AgentProviderDefaultSupport, AgentProviderSource, AgentProviderState, + AgentProviderDefaultSupport, AgentProviderModel, AgentProviderSource, + AgentProviderState, BuiltInProviderSupport, }; use crate::credentials::CredentialStore; use crate::error::Result; @@ -32,6 +33,9 @@ use crate::store::{InferenceProviderRepository, InferenceProviderStore}; pub(super) const AGENT_ID: &str = "claude"; +/// Provider ID for the built-in official login (OAuth via Keychain). +pub const OFFICIAL_LOGIN_PROVIDER_ID: &str = "official_login"; + const API_BASE_URL_ENV: &str = "ANTHROPIC_BASE_URL"; const API_KEY_ENV: &str = "ANTHROPIC_API_KEY"; const AUTH_TOKEN_ENV: &str = "ANTHROPIC_AUTH_TOKEN"; @@ -71,6 +75,25 @@ pub struct ClaudeConfigState { pub opus_model: Option, } +/// Built-in binding representing the official Anthropic login (OAuth). +/// +/// When no API key override exists in `settings.json`, Claude Code +/// automatically falls back to the OAuth token stored in macOS Keychain. +fn built_in_official_login_binding() -> AgentProviderBinding { + AgentProviderBinding { + id: OFFICIAL_LOGIN_PROVIDER_ID.to_string(), + source_provider_id: None, + name: "Official Login".to_string(), + format: Some(crate::model::InferenceProviderFormat::Anthropic), + api_base_url: None, + credential: AgentProviderCredential::AgentStore { + id: Some(OFFICIAL_LOGIN_PROVIDER_ID.to_string()), + }, + models: Vec::::new(), + source: AgentProviderSource::BuiltIn, + } +} + impl ClaudeProviderAdapter { /// Create an adapter with an explicit config path. pub fn new(config_path: impl Into) -> Self { @@ -223,6 +246,10 @@ impl ClaudeProviderAdapter { /// Load bindings from the SQLite store and derive the active one from /// `settings.json`. Returns the effective provider state. + /// + /// The built-in "Official Login" provider is always prepended. It is + /// considered active when no binding row matches the current config + /// (i.e. no API key override is present in `settings.json`). pub fn load_bindings_state( &self, store: &InferenceProviderStore, @@ -230,10 +257,10 @@ impl ClaudeProviderAdapter { let rows = store.list_agent_bindings(AGENT_ID)?; let active_row = self.derive_active_binding(store, &rows)?; - let providers = rows - .iter() - .map(|row| store.binding_from_row(row)) - .collect::>>()?; + let mut providers = vec![built_in_official_login_binding()]; + for row in &rows { + providers.push(store.binding_from_row(row)?); + } let default_model = active_row .as_ref() @@ -256,6 +283,19 @@ impl ClaudeProviderAdapter { Ok(self.derive_active_binding(store, &rows)?.map(|row| row.id)) } + /// Derive the active provider ID from current `settings.json` state. + /// + /// Returns `"official_login"` when no binding matches (OAuth fallback), + /// or the matching binding's ID when an API key override is active. + pub fn derive_active_provider_id( + &self, + store: &InferenceProviderStore, + ) -> Result { + Ok(self + .active_binding_id(store)? + .unwrap_or_else(|| OFFICIAL_LOGIN_PROVIDER_ID.to_string())) + } + /// Add a new binding and optionally sync it into `settings.json`. pub fn add_binding( &self, @@ -279,11 +319,19 @@ impl ClaudeProviderAdapter { } /// Switch the active provider by rewriting `settings.json`. + /// + /// When `binding_id` is `"official_login"`, all API key overrides are + /// cleared so Claude Code falls back to its native OAuth token. pub fn set_active_binding( &self, store: &InferenceProviderStore, binding_id: &str, ) -> Result { + if binding_id == OFFICIAL_LOGIN_PROVIDER_ID { + self.clear_provider_config()?; + return self.load_bindings_state(store); + } + let row = store .list_agent_bindings(AGENT_ID)? .into_iter() @@ -351,7 +399,7 @@ impl AgentProviderAdapter for ClaudeProviderAdapter { AgentProviderCapabilities::registry( AgentProviderDefaultSupport::MODEL_ONLY, AgentCredentialSupport::ENV_VAR, - crate::agent::BuiltInProviderSupport::NONE, + BuiltInProviderSupport::IMMUTABLE, ) } diff --git a/crates/inference/src/claude/tests.rs b/crates/inference/src/claude/tests.rs index a9d1f9df..13ac80c3 100644 --- a/crates/inference/src/claude/tests.rs +++ b/crates/inference/src/claude/tests.rs @@ -336,3 +336,129 @@ fn active_binding_id_tracks_switches_without_model_selection() { Some("https://api.two.example") ); } + +#[test] +fn official_login_appears_first_in_provider_list() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + let store = store(&temp); + fs::write(adapter.config_path(), "{}").unwrap(); + + let state = adapter.load_bindings_state(&store).unwrap(); + assert!(!state.providers.is_empty()); + assert_eq!(state.providers[0].id, OFFICIAL_LOGIN_PROVIDER_ID); + assert_eq!(state.providers[0].source, AgentProviderSource::BuiltIn); + assert_eq!( + state.providers[0].format, + Some(InferenceProviderFormat::Anthropic) + ); +} + +#[test] +fn official_login_active_when_no_api_key_override() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + let store = store(&temp); + fs::write( + adapter.config_path(), + r#"{ "permissions": { "allow": ["Read"] } }"#, + ) + .unwrap(); + + let active_id = adapter.derive_active_provider_id(&store).unwrap(); + assert_eq!(active_id, OFFICIAL_LOGIN_PROVIDER_ID); +} + +#[test] +fn switch_to_official_login_clears_env_block() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + let store = store(&temp); + fs::write( + adapter.config_path(), + r#"{ + "model": "claude-sonnet-4-5", + "permissions": { "allow": ["Read"] }, + "env": { + "ANTHROPIC_BASE_URL": "https://api.example.com", + "ANTHROPIC_AUTH_TOKEN": "sk-test", + "ANTHROPIC_MODEL": "claude-sonnet-4-5", + "KEEP": "1" + } + }"#, + ) + .unwrap(); + + adapter + .set_active_binding(&store, OFFICIAL_LOGIN_PROVIDER_ID) + .unwrap(); + + let config: Value = serde_json::from_str( + &fs::read_to_string(adapter.config_path()).unwrap(), + ) + .unwrap(); + let env = config["env"].as_object().unwrap(); + assert!(!env.contains_key("ANTHROPIC_AUTH_TOKEN")); + assert!(!env.contains_key("ANTHROPIC_BASE_URL")); + assert!(!env.contains_key("ANTHROPIC_MODEL")); + assert_eq!(env.get("KEEP").and_then(Value::as_str), Some("1")); + assert!(config.get("permissions").is_some()); + assert!(config.get("model").is_none()); + + let active_id = adapter.derive_active_provider_id(&store).unwrap(); + assert_eq!(active_id, OFFICIAL_LOGIN_PROVIDER_ID); +} + +#[test] +fn switch_from_official_to_api_via_sync_writes_env() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + let store = store(&temp); + fs::write(adapter.config_path(), "{}").unwrap(); + + // Directly sync a provider into settings.json (bypasses store create). + let provider = InferenceProvider { + id: "inv-anthropic".to_string(), + name: "my-anthropic".to_string(), + display_name: "My Anthropic".to_string(), + format: InferenceProviderFormat::Anthropic, + api_base_url: "https://api.anthropic.com".to_string(), + masked_api_key: "sk****st".to_string(), + models: vec!["claude-sonnet-4-5".to_string()], + }; + adapter + .sync_active_binding( + &provider, + "sk-live-key", + Some("claude-sonnet-4-5"), + ) + .unwrap(); + + // Verify settings.json has the API key. + let config: Value = serde_json::from_str( + &fs::read_to_string(adapter.config_path()).unwrap(), + ) + .unwrap(); + assert_eq!( + config["env"]["ANTHROPIC_AUTH_TOKEN"].as_str(), + Some("sk-live-key") + ); + assert_eq!( + config["env"]["ANTHROPIC_BASE_URL"].as_str(), + Some("https://api.anthropic.com") + ); + + // Switch back to official login. + adapter + .set_active_binding(&store, OFFICIAL_LOGIN_PROVIDER_ID) + .unwrap(); + let active_id = adapter.derive_active_provider_id(&store).unwrap(); + assert_eq!(active_id, OFFICIAL_LOGIN_PROVIDER_ID); + + // settings.json env block should be cleared. + let config: Value = serde_json::from_str( + &fs::read_to_string(adapter.config_path()).unwrap(), + ) + .unwrap(); + assert!(config.get("env").is_none()); +} From 82bb8433541ba4a39e86c97dc410504427fdcc2f Mon Sep 17 00:00:00 2001 From: akarachen Date: Sat, 2 May 2026 22:57:04 +0800 Subject: [PATCH 34/62] fix(codex): write provider keys inline --- crates/inference/src/codex/mapping.rs | 9 +++++++-- crates/inference/src/codex/tests.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/inference/src/codex/mapping.rs b/crates/inference/src/codex/mapping.rs index ef2c016d..4d38e959 100644 --- a/crates/inference/src/codex/mapping.rs +++ b/crates/inference/src/codex/mapping.rs @@ -207,8 +207,13 @@ fn apply_credential( ) { match credential { AgentProviderCredential::EnvVar { name } => { - table["env_key"] = value(name.clone()); - table.remove("experimental_bearer_token"); + if let Some(api_key) = api_key { + table.remove("env_key"); + table["experimental_bearer_token"] = value(api_key.to_string()); + } else { + table["env_key"] = value(name.clone()); + table.remove("experimental_bearer_token"); + } table.remove("requires_openai_auth"); table.remove("auth"); remove_authorization_header(table); diff --git a/crates/inference/src/codex/tests.rs b/crates/inference/src/codex/tests.rs index 58a03a40..273f9148 100644 --- a/crates/inference/src/codex/tests.rs +++ b/crates/inference/src/codex/tests.rs @@ -465,6 +465,32 @@ model_provider = "openai" assert_eq!(config["model"].as_str(), Some("gpt-5")); } +#[test] +fn set_active_provider_writes_inventory_key_inline() { + let temp = tempfile::tempdir().unwrap(); + let adapter = adapter(&temp); + fs::write(adapter.config_path(), r#"model_provider = "openai""#).unwrap(); + + let store = store(&temp); + let provider = create_inventory_provider(&store); + adapter + .add_inventory_provider(&store, &provider, "sk-test") + .unwrap(); + + adapter.set_active_provider(&store, "openrouter").unwrap(); + + let config = fs::read_to_string(adapter.config_path()) + .unwrap() + .parse::() + .unwrap(); + let provider = config["model_providers"]["openrouter"].as_table().unwrap(); + assert_eq!( + provider["experimental_bearer_token"].as_str(), + Some("sk-test") + ); + assert!(provider.get("env_key").is_none()); +} + #[test] fn clear_active_provider_falls_back_to_openai() { let temp = tempfile::tempdir().unwrap(); From ec6d9c1cab5895696c58dfed8b80d2558ea9bf26 Mon Sep 17 00:00:00 2001 From: danielchim Date: Sun, 3 May 2026 20:05:47 +0800 Subject: [PATCH 35/62] feat(desktop): show config folder button on Codex provider rows Adds a folder button to each row that reveals ~/.codex/config.toml in the OS file browser. Granted via opener:allow-reveal-item-in-dir capability. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src-tauri/capabilities/default.json | 1 + crates/desktop/src/lib/locales/en.ts | 2 ++ crates/desktop/src/lib/locales/zh-Hans.ts | 2 ++ crates/desktop/src/lib/locales/zh-Hant.ts | 2 ++ .../pages/inference-providers/codex-panel.tsx | 32 +++++++++++++++++++ 5 files changed, 39 insertions(+) diff --git a/crates/desktop/src-tauri/capabilities/default.json b/crates/desktop/src-tauri/capabilities/default.json index 9bff37f9..9c1c7cbe 100644 --- a/crates/desktop/src-tauri/capabilities/default.json +++ b/crates/desktop/src-tauri/capabilities/default.json @@ -17,6 +17,7 @@ "deep-link:default", "opener:default", "opener:allow-open-path", + "opener:allow-reveal-item-in-dir", "dialog:default", "log:default", "store:default", diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index d95b4655..69d7ea9e 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -119,6 +119,8 @@ export default { createCodexProvider: "Add Codex Provider", editCodexProvider: "Edit Codex Provider", deleteCodexProvider: "Delete Codex Provider", + showConfigFolder: "Show config folder", + showConfigFolderFailed: "Failed to open config folder", deleteCodexProviderConfirm: 'Delete "{{name}}" from Codex config.toml?', refreshCodexProviders: "Refresh Codex providers", noCodexProviders: "No Codex providers configured.", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index e6373def..610af81d 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -114,6 +114,8 @@ export default { createCodexProvider: "添加 Codex Provider", editCodexProvider: "编辑 Codex Provider", deleteCodexProvider: "删除 Codex Provider", + showConfigFolder: "显示配置文件夹", + showConfigFolderFailed: "无法打开配置文件夹", deleteCodexProviderConfirm: '确定从 Codex config.toml 删除"{{name}}"吗?', refreshCodexProviders: "刷新 Codex Provider", noCodexProviders: "暂无 Codex Provider。", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index bd741864..6e3a6f82 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -114,6 +114,8 @@ export default { createCodexProvider: "新增 Codex Provider", editCodexProvider: "編輯 Codex Provider", deleteCodexProvider: "刪除 Codex Provider", + showConfigFolder: "顯示設定資料夾", + showConfigFolderFailed: "無法開啟設定資料夾", deleteCodexProviderConfirm: "確定從 Codex config.toml 刪除「{{name}}」嗎?", refreshCodexProviders: "重新整理 Codex Provider", noCodexProviders: "尚無 Codex Provider。", diff --git a/crates/desktop/src/pages/inference-providers/codex-panel.tsx b/crates/desktop/src/pages/inference-providers/codex-panel.tsx index c24218be..85f3f801 100644 --- a/crates/desktop/src/pages/inference-providers/codex-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/codex-panel.tsx @@ -1,11 +1,14 @@ import { ArrowPathIcon, CheckCircleIcon, + FolderOpenIcon, PlayIcon, PlusIcon, ServerIcon, TrashIcon, } from "@heroicons/react/24/solid"; +import { homeDir, join } from "@tauri-apps/api/path"; +import { revealItemInDir } from "@tauri-apps/plugin-opener"; import { Alert, AlertDialog, @@ -298,6 +301,21 @@ function CodexProviderRow({ const model = provider.models[0]?.id ?? null; const isExternal = provider.source === "external"; + const handleShowFolder = async () => { + try { + const home = await homeDir(); + const configPath = await join(home, ".codex", "config.toml"); + await revealItemInDir(configPath); + } catch (error) { + console.error("Failed to reveal codex config folder:", error); + toast.danger( + error instanceof Error + ? error.message + : t("showConfigFolderFailed"), + ); + } + }; + return (

@@ -329,6 +347,20 @@ function CodexProviderRow({
+ + + + + {t("showConfigFolder")} + {matchedProvider && !isExternal && ( From 8c285863a134575659444381b4d458beec19701b Mon Sep 17 00:00:00 2001 From: danielchim Date: Sun, 3 May 2026 20:07:22 +0800 Subject: [PATCH 36/62] feat(desktop): overhaul inference provider form UX - Coding Agents list converted to a Select dropdown - ProviderModelsEditor: fetch models from endpoint, search field, per-row checkboxes, batch delete with confirmation, vendor-prefix accordion grouping (e.g. anthropic/claude-3.5) - ProviderForm: Quick start preset card with logo + name, Get API key link tied to selected preset, friendly duplicate-name error message Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/desktop/src/lib/locales/en.ts | 31 + crates/desktop/src/lib/locales/zh-Hans.ts | 31 + crates/desktop/src/lib/locales/zh-Hant.ts | 30 + .../desktop/src/pages/inference-providers.tsx | 838 +++++++++++++++--- 4 files changed, 828 insertions(+), 102 deletions(-) diff --git a/crates/desktop/src/lib/locales/en.ts b/crates/desktop/src/lib/locales/en.ts index 69d7ea9e..e131b6e1 100644 --- a/crates/desktop/src/lib/locales/en.ts +++ b/crates/desktop/src/lib/locales/en.ts @@ -93,6 +93,7 @@ export default { "Choose your preferred code editor for opening files", inferenceProviders: "Inference Providers", codingAgents: "Coding Agents", + selectCodingAgent: "Select a coding agent", openCodeProvidersDescription: "Providers configured in opencode.json and auth.json.", createOpenCodeProvider: "Add OpenCode Provider", @@ -204,6 +205,12 @@ export default { searchInferenceProviders: "Search providers...", refreshInferenceProviders: "Refresh providers", createInferenceProvider: "Add Provider", + providerPresetsTitle: "Quick start", + providerPresetsDescription: + "Pick a preset to fill in the name, base URL, format, and models.", + providerPresetsPlaceholder: "Choose a preset", + providerPresetsNone: "None", + providerGetApiKey: "Get API key →", createInferenceProviderDescription: "Add an inference endpoint and store its API key securely.", editInferenceProvider: "Edit Provider", @@ -228,7 +235,29 @@ export default { providerModelName: "Model name", providerModelNamePlaceholder: "e.g., gpt-5.4-mini", addProviderModel: "Add model", + fetchProviderModels: "Fetch models", + fetchProviderModelsPending: "Fetching models…", + fetchProviderModelsSuccess: + "Added {{added}} new model(s) ({{total}} returned)", + fetchProviderModelsSuccessNoNew: + "No new models to add ({{total}} already configured)", + fetchProviderModelsFailed: "Failed to fetch models: {{reason}}", + fetchProviderModelsUnknownError: "endpoint not supported", noProviderModels: "No models configured.", + noProviderModelsMatch: "No models match your search.", + providerModelGroupUncategorized: "Uncategorized", + providerModelGroupCount: "{{count}} model(s)", + providerModelGroupCountWithSelected: "{{selected}} of {{total}} selected", + searchProviderModels: "Search models", + searchProviderModelsPlaceholder: "Search models…", + selectAllProviderModels: "Select all visible models", + deselectAllProviderModels: "Deselect all visible models", + selectProviderModel: 'Select "{{name}}"', + selectedProviderModelsCount: "{{selected}} of {{total}} selected", + deleteSelectedProviderModels: "Delete selected ({{count}})", + confirmDeleteProviderModels: "Delete selected models?", + confirmDeleteProviderModelsBody: + "This will remove {{count}} model(s) from the list. The change is saved when you submit the form.", providerModelNameCopied: "Model name copied", providerModelNameCopyFailed: "Failed to copy model name", copyProviderModelName: 'Copy "{{name}}"', @@ -239,6 +268,8 @@ export default { hideProviderApiKey: "Hide API key", inferenceProviderCreated: "Provider created", inferenceProviderUpdated: "Provider updated", + inferenceProviderDuplicateError: + "A provider named “{{name}}” is already configured. Please choose a different name or edit the existing entry.", inferenceProviderDeleted: "Provider deleted", deleteInferenceProviderError: "Failed to delete provider", inferenceProviderPasswordLoadFailed: "Failed to load API key", diff --git a/crates/desktop/src/lib/locales/zh-Hans.ts b/crates/desktop/src/lib/locales/zh-Hans.ts index 610af81d..60b71176 100644 --- a/crates/desktop/src/lib/locales/zh-Hans.ts +++ b/crates/desktop/src/lib/locales/zh-Hans.ts @@ -89,6 +89,7 @@ export default { codeEditorsDescription: "选择用于打开文件的首选代码编辑器", inferenceProviders: "推理 Provider", codingAgents: "Coding Agents", + selectCodingAgent: "选择 Coding Agent", openCodeProvidersDescription: "配置在 opencode.json 和 auth.json 里的 Provider。", createOpenCodeProvider: "添加 OpenCode Provider", @@ -196,6 +197,12 @@ export default { searchInferenceProviders: "搜索 Provider...", refreshInferenceProviders: "刷新 Provider", createInferenceProvider: "添加 Provider", + providerPresetsTitle: "快速开始", + providerPresetsDescription: + "选择一个预设以自动填充名称、Base URL、格式和模型。", + providerPresetsPlaceholder: "选择一个预设", + providerPresetsNone: "不使用预设", + providerGetApiKey: "获取 API Key →", createInferenceProviderDescription: "添加一个推理端点,并安全存储它的 API key。", editInferenceProvider: "编辑 Provider", @@ -220,7 +227,29 @@ export default { providerModelName: "模型名称", providerModelNamePlaceholder: "例如:gpt-5.4-mini", addProviderModel: "添加模型", + fetchProviderModels: "获取模型", + fetchProviderModelsPending: "正在获取模型…", + fetchProviderModelsSuccess: + "已新增 {{added}} 个模型(共返回 {{total}} 个)", + fetchProviderModelsSuccessNoNew: + "没有新模型需要添加(已配置 {{total}} 个)", + fetchProviderModelsFailed: "获取模型失败:{{reason}}", + fetchProviderModelsUnknownError: "端点不支持", noProviderModels: "暂无模型。", + noProviderModelsMatch: "没有匹配的模型。", + providerModelGroupUncategorized: "未分类", + providerModelGroupCount: "{{count}} 个模型", + providerModelGroupCountWithSelected: "已选 {{selected}} / {{total}}", + searchProviderModels: "搜索模型", + searchProviderModelsPlaceholder: "搜索模型…", + selectAllProviderModels: "全选当前可见模型", + deselectAllProviderModels: "取消全选当前可见模型", + selectProviderModel: '选择 "{{name}}"', + selectedProviderModelsCount: "已选 {{selected}} / {{total}}", + deleteSelectedProviderModels: "删除所选 ({{count}})", + confirmDeleteProviderModels: "删除所选模型?", + confirmDeleteProviderModelsBody: + "将从列表中移除 {{count}} 个模型,提交表单后保存更改。", providerModelNameCopied: "模型名称已复制", providerModelNameCopyFailed: "复制模型名称失败", copyProviderModelName: '复制"{{name}}"', @@ -231,6 +260,8 @@ export default { hideProviderApiKey: "隐藏 API key", inferenceProviderCreated: "Provider 已创建", inferenceProviderUpdated: "Provider 已更新", + inferenceProviderDuplicateError: + "已存在名为 “{{name}}” 的 Provider。请使用其他名称,或直接编辑已有条目。", inferenceProviderDeleted: "Provider 已删除", deleteInferenceProviderError: "删除 Provider 失败", inferenceProviderPasswordLoadFailed: "读取 API key 失败", diff --git a/crates/desktop/src/lib/locales/zh-Hant.ts b/crates/desktop/src/lib/locales/zh-Hant.ts index 6e3a6f82..79890707 100644 --- a/crates/desktop/src/lib/locales/zh-Hant.ts +++ b/crates/desktop/src/lib/locales/zh-Hant.ts @@ -89,6 +89,7 @@ export default { codeEditorsDescription: "選擇用於開啟檔案的偏好程式碼編輯器", inferenceProviders: "推理 Provider", codingAgents: "Coding Agents", + selectCodingAgent: "選擇 Coding Agent", openCodeProvidersDescription: "配置在 opencode.json 和 auth.json 裡的 Provider。", createOpenCodeProvider: "新增 OpenCode Provider", @@ -196,6 +197,12 @@ export default { searchInferenceProviders: "搜尋 Provider...", refreshInferenceProviders: "重新整理 Provider", createInferenceProvider: "新增 Provider", + providerPresetsTitle: "快速開始", + providerPresetsDescription: + "選擇一個預設以自動填入名稱、Base URL、格式和模型。", + providerPresetsPlaceholder: "選擇一個預設", + providerPresetsNone: "不使用預設", + providerGetApiKey: "取得 API Key →", createInferenceProviderDescription: "新增一個推理端點,並安全儲存它的 API key。", editInferenceProvider: "編輯 Provider", @@ -220,7 +227,28 @@ export default { providerModelName: "模型名稱", providerModelNamePlaceholder: "例如:gpt-5.4-mini", addProviderModel: "新增模型", + fetchProviderModels: "擷取模型", + fetchProviderModelsPending: "正在擷取模型…", + fetchProviderModelsSuccess: + "已新增 {{added}} 個模型(共回傳 {{total}} 個)", + fetchProviderModelsSuccessNoNew: "沒有新模型需新增(已設定 {{total}} 個)", + fetchProviderModelsFailed: "擷取模型失敗:{{reason}}", + fetchProviderModelsUnknownError: "端點不支援", noProviderModels: "尚無模型。", + noProviderModelsMatch: "沒有相符的模型。", + providerModelGroupUncategorized: "未分類", + providerModelGroupCount: "{{count}} 個模型", + providerModelGroupCountWithSelected: "已選 {{selected}} / {{total}}", + searchProviderModels: "搜尋模型", + searchProviderModelsPlaceholder: "搜尋模型…", + selectAllProviderModels: "全選目前可見模型", + deselectAllProviderModels: "取消全選目前可見模型", + selectProviderModel: '選擇 "{{name}}"', + selectedProviderModelsCount: "已選 {{selected}} / {{total}}", + deleteSelectedProviderModels: "刪除所選 ({{count}})", + confirmDeleteProviderModels: "刪除所選模型?", + confirmDeleteProviderModelsBody: + "將從清單中移除 {{count}} 個模型,提交表單後儲存變更。", providerModelNameCopied: "模型名稱已複製", providerModelNameCopyFailed: "複製模型名稱失敗", copyProviderModelName: "複製「{{name}}」", @@ -231,6 +259,8 @@ export default { hideProviderApiKey: "隱藏 API key", inferenceProviderCreated: "Provider 已建立", inferenceProviderUpdated: "Provider 已更新", + inferenceProviderDuplicateError: + "已存在名為「{{name}}」的 Provider。請改用其他名稱,或直接編輯既有項目。", inferenceProviderDeleted: "Provider 已刪除", deleteInferenceProviderError: "刪除 Provider 失敗", inferenceProviderPasswordLoadFailed: "讀取 API key 失敗", diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index fb70e7ca..a77503ad 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -1,6 +1,7 @@ import { ArrowPathIcon, ClipboardDocumentIcon, + CloudArrowDownIcon, CpuChipIcon, EyeIcon, EyeSlashIcon, @@ -10,18 +11,26 @@ import { TrashIcon, } from "@heroicons/react/24/solid"; import AnthropicIcon from "@lobehub/icons/es/Anthropic"; +import DeepSeekIcon from "@lobehub/icons/es/DeepSeek"; +import GroqIcon from "@lobehub/icons/es/Groq"; +import MistralIcon from "@lobehub/icons/es/Mistral"; import OpenAIIcon from "@lobehub/icons/es/OpenAI"; +import OpenRouterIcon from "@lobehub/icons/es/OpenRouter"; +import TogetherIcon from "@lobehub/icons/es/Together"; import { + Accordion, Alert, AlertDialog, Button, Card, + Checkbox, FieldError, Fieldset, Form, Input, Label, ListBox, + SearchField, Select, Spinner, TextField, @@ -30,8 +39,9 @@ import { } from "@heroui/react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import Fuse from "fuse.js"; -import { useMemo, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; +import type React from "react"; +import { type Key, useMemo, useState } from "react"; +import { Controller, useForm, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { ListSearchHeader } from "../components/list-search-header"; import { ResourceSectionHeader } from "../components/resource-section-header"; @@ -49,8 +59,11 @@ import { createInferenceProviderMutationOptions, deleteInferenceProviderMutationOptions, inferenceProviderListQueryOptions, + inferenceProviderPresetsQueryOptions, updateInferenceProviderMutationOptions, } from "../requests/inference-providers"; +import { openUrl } from "@tauri-apps/plugin-opener"; +import type { InferenceProviderPresetResponse } from "../generated/dto"; import { useAgentAvailability } from "../hooks/use-agent-availability"; type CodingAgentId = "opencode" | "codex" | "claude"; @@ -90,6 +103,50 @@ const FORMAT_OPTIONS: FormatOption[] = [ }, ]; +async function fetchProviderModels({ + format, + apiBaseUrl, + apiKey, +}: { + format: InferenceProviderFormatDto; + apiBaseUrl: string; + apiKey: string; +}): Promise { + const trimmedBase = apiBaseUrl.trim().replace(/\/+$/, ""); + if (!trimmedBase) throw new Error("Missing API base URL"); + if (!apiKey.trim()) throw new Error("Missing API key"); + + const headers: Record = + format === "anthropic" + ? { + "x-api-key": apiKey, + "anthropic-version": "2023-06-01", + } + : { + Authorization: `Bearer ${apiKey}`, + }; + + const response = await fetch(`${trimmedBase}/models`, { + method: "GET", + headers, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const payload = (await response.json()) as { + data?: Array<{ id?: unknown }>; + }; + const data = Array.isArray(payload?.data) ? payload.data : []; + const ids = data + .map((entry) => (typeof entry?.id === "string" ? entry.id : null)) + .filter((id): id is string => Boolean(id)); + + if (ids.length === 0) throw new Error("No models in response"); + return ids; +} + const CODING_AGENT_OPTIONS: CodingAgentOption[] = [ { id: "opencode", @@ -139,6 +196,32 @@ function ProviderIcon({ format }: { format: InferenceProviderFormatDto }) { ); } +const PRESET_LOGO_MAP: Record< + string, + React.ComponentType<{ size?: number; "aria-hidden"?: boolean }> +> = { + OpenAI: OpenAIIcon, + Anthropic: AnthropicIcon, + OpenRouter: OpenRouterIcon, + Groq: GroqIcon, + Mistral: MistralIcon, + Together: TogetherIcon, + DeepSeek: DeepSeekIcon, +}; + +function PresetLogo({ logo, size = 16 }: { logo: string; size?: number }) { + const Icon = PRESET_LOGO_MAP[logo]; + return ( +
+ {Icon ? ( + + ) : ( + + )} +
+ ); +} + function MonoValue({ children, className, @@ -178,25 +261,195 @@ function validateModelNames(models: ProviderModelFormValue[], message: string) { return true; } +const UNCATEGORIZED_GROUP_KEY = "__uncategorized__"; + +interface ProviderModelGroup { + key: string; + label: string; + isUncategorized: boolean; + items: ProviderModelFormValue[]; +} + +function groupProviderModels( + models: ProviderModelFormValue[], +): ProviderModelGroup[] { + const buckets = new Map(); + for (const model of models) { + const name = model.name.trim(); + const slashIndex = name.indexOf("/"); + const isPrefixed = slashIndex > 0 && slashIndex < name.length - 1; + const key = isPrefixed + ? name.slice(0, slashIndex) + : UNCATEGORIZED_GROUP_KEY; + const existing = buckets.get(key); + if (existing) { + existing.items.push(model); + } else { + buckets.set(key, { + key, + label: isPrefixed ? key : "", + isUncategorized: !isPrefixed, + items: [model], + }); + } + } + + const groups = Array.from(buckets.values()); + groups.sort((a, b) => { + if (a.isUncategorized) return 1; + if (b.isUncategorized) return -1; + return a.label.localeCompare(b.label); + }); + return groups; +} + function ProviderModelsEditor({ value, onChange, onBlur, errorMessage, + onFetchModels, + canFetchModels = false, }: { value: ProviderModelFormValue[]; onChange: (value: ProviderModelFormValue[]) => void; onBlur: () => void; errorMessage?: string; + onFetchModels?: () => Promise; + canFetchModels?: boolean; }) { const { t } = useTranslation(); const emptyModel = useMemo(() => createProviderModelFormValue(), []); - const displayModels = value.length > 0 ? value : [emptyModel]; + const [isFetching, setIsFetching] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedIds, setSelectedIds] = useState>(new Set()); + const [isBatchDeleteOpen, setIsBatchDeleteOpen] = useState(false); + const [collapsedGroups, setCollapsedGroups] = useState>( + new Set(), + ); + + const hasRealModels = value.length > 0; + const trimmedQuery = searchQuery.trim().toLowerCase(); + const filteredModels = useMemo(() => { + if (!hasRealModels) return []; + if (!trimmedQuery) return value; + return value.filter((model) => + model.name.toLowerCase().includes(trimmedQuery), + ); + }, [hasRealModels, trimmedQuery, value]); + + const displayModels = hasRealModels ? filteredModels : [emptyModel]; + + const filteredSelectedCount = filteredModels.reduce( + (count, model) => count + (selectedIds.has(model.id) ? 1 : 0), + 0, + ); + const totalSelectedCount = value.reduce( + (count, model) => count + (selectedIds.has(model.id) ? 1 : 0), + 0, + ); + const allFilteredSelected = + filteredModels.length > 0 && + filteredSelectedCount === filteredModels.length; + const someFilteredSelected = + filteredSelectedCount > 0 && !allFilteredSelected; + + const filteredGroups = useMemo( + () => groupProviderModels(filteredModels), + [filteredModels], + ); + const useAccordion = hasRealModels && filteredGroups.length > 1; + const expandedGroupKeys = useMemo( + () => + filteredGroups + .map((group) => group.key) + .filter((key) => !collapsedGroups.has(key)), + [filteredGroups, collapsedGroups], + ); + + const handleExpandedChange = (next: Set) => { + const visibleKeys = filteredGroups.map((group) => group.key); + setCollapsedGroups((prev) => { + const result = new Set(prev); + for (const key of visibleKeys) { + if (next.has(key)) result.delete(key); + else result.add(key); + } + return result; + }); + }; const handleAdd = () => { onChange([...value, createProviderModelFormValue()]); }; + const toggleSelected = (id: string, selected: boolean) => { + setSelectedIds((prev) => { + const next = new Set(prev); + if (selected) next.add(id); + else next.delete(id); + return next; + }); + }; + + const handleToggleAllFiltered = (selected: boolean) => { + setSelectedIds((prev) => { + const next = new Set(prev); + for (const model of filteredModels) { + if (selected) next.add(model.id); + else next.delete(model.id); + } + return next; + }); + }; + + const handleBatchDelete = () => { + if (totalSelectedCount === 0) return; + const remaining = value.filter((model) => !selectedIds.has(model.id)); + onChange(remaining); + setSelectedIds(new Set()); + setIsBatchDeleteOpen(false); + }; + + const handleFetch = async () => { + if (!onFetchModels) return; + setIsFetching(true); + try { + const fetched = await onFetchModels(); + const existing = new Set( + value.map((model) => model.name.trim()).filter(Boolean), + ); + const additions = fetched + .filter((name) => !existing.has(name)) + .map((name) => createProviderModelFormValue(name)); + const baseline = value.filter((model) => model.name.trim()); + onChange([...baseline, ...additions]); + if (additions.length === 0) { + toast.success( + t("fetchProviderModelsSuccessNoNew", { + total: fetched.length, + }), + ); + } else { + toast.success( + t("fetchProviderModelsSuccess", { + added: additions.length, + total: fetched.length, + }), + ); + } + } catch (error) { + console.error("Failed to fetch provider models:", error); + const reason = + error instanceof Error && error.message + ? error.message + : t("fetchProviderModelsUnknownError"); + toast.danger(t("fetchProviderModelsFailed", { reason })); + } finally { + setIsFetching(false); + } + }; + const handleRemove = (id: string) => { onChange(value.filter((model) => model.id !== id)); }; @@ -211,60 +464,273 @@ function ProviderModelsEditor({ onChange(nextModels); }; - return ( -
-
-
- -

- {t("providerModelsDescription")} -

-
- -
+ + + + + )} + handleChange(model.id, event.target.value)} + onBlur={onBlur} + placeholder={t("providerModelNamePlaceholder")} + aria-label={t("providerModelName")} + variant="secondary" + className="min-w-0 flex-1" + /> + +
+ ); + return ( + <>
- {displayModels.map((model) => ( -
- - handleChange(model.id, event.target.value) - } - onBlur={onBlur} - placeholder={t("providerModelNamePlaceholder")} - aria-label={t("providerModelName")} - variant="secondary" - className="min-w-0 flex-1" - /> +
+
+ +

+ {t("providerModelsDescription")} +

+
+
+ {onFetchModels && ( + + )}
- ))} +
+ + {hasRealModels && ( + + + + + + + + )} + + {hasRealModels && ( +
+ + + + + + + {t("selectedProviderModelsCount", { + selected: totalSelectedCount, + total: value.length, + })} + + + + {totalSelectedCount > 0 && ( + + )} +
+ )} + +
+ {hasRealModels && filteredModels.length === 0 ? ( +

+ {t("noProviderModelsMatch")} +

+ ) : useAccordion ? ( + + {filteredGroups.map((group) => { + const groupSelected = group.items.reduce( + (count, model) => + count + + (selectedIds.has(model.id) ? 1 : 0), + 0, + ); + const groupLabel = group.isUncategorized + ? t("providerModelGroupUncategorized") + : group.label; + return ( + + + + + {groupLabel} + + + {groupSelected > 0 + ? t( + "providerModelGroupCountWithSelected", + { + selected: + groupSelected, + total: group + .items + .length, + }, + ) + : t( + "providerModelGroupCount", + { + count: group + .items + .length, + }, + )} + + + + + + +
+ {group.items.map((model) => + renderModelRow(model), + )} +
+
+
+
+ ); + })} +
+ ) : ( + displayModels.map((model) => renderModelRow(model)) + )} +
+ + {errorMessage && ( +

{errorMessage}

+ )}
- {errorMessage && ( -

{errorMessage}

- )} -
+ + + + + + + + {t("confirmDeleteProviderModels")} + + + + {t("confirmDeleteProviderModelsBody", { + count: totalSelectedCount, + })} + + + + + + + + + ); } @@ -285,6 +751,8 @@ function ProviderForm({ const { control, handleSubmit, + getValues, + setValue, formState: { isSubmitting }, } = useForm({ mode: "onSubmit", @@ -298,6 +766,51 @@ function ProviderForm({ }, }); + const [selectedPresetId, setSelectedPresetId] = useState( + null, + ); + const { data: presets = [] } = useQuery({ + ...inferenceProviderPresetsQueryOptions({ api }), + }); + const selectedPreset = useMemo( + () => presets.find((preset) => preset.id === selectedPresetId) ?? null, + [presets, selectedPresetId], + ); + + const handleApplyPreset = (preset: InferenceProviderPresetResponse) => { + setValue("displayName", preset.name, { shouldDirty: true }); + setValue("apiBaseUrl", preset.api_base_url, { shouldDirty: true }); + setValue("format", preset.format, { shouldDirty: true }); + setValue("models", toProviderModelFormValues(preset.models), { + shouldDirty: true, + }); + setSelectedPresetId(preset.id); + }; + + const handlePresetSelectionChange = (key: string | null) => { + if (!key || key === "__none__") { + setSelectedPresetId(null); + return; + } + const preset = presets.find((candidate) => candidate.id === key); + if (preset) handleApplyPreset(preset); + }; + + const watchedApiBaseUrl = useWatch({ control, name: "apiBaseUrl" }); + const watchedApiKey = useWatch({ control, name: "apiKey" }); + const canFetchModels = Boolean( + watchedApiBaseUrl?.trim() && watchedApiKey?.trim(), + ); + + const handleFetchModels = async () => { + const values = getValues(); + return fetchProviderModels({ + format: values.format, + apiBaseUrl: values.apiBaseUrl, + apiKey: values.apiKey, + }); + }; + const createMutation = useMutation({ ...createInferenceProviderMutationOptions({ api, @@ -356,6 +869,21 @@ function ProviderForm({ } }; + const rawErrorMessage = + activeError instanceof Error + ? activeError.message + : activeError + ? String(activeError) + : ""; + const duplicateMatch = rawErrorMessage.match( + /provider already exists:\s*(.+)/i, + ); + const friendlyErrorMessage = duplicateMatch + ? t("inferenceProviderDuplicateError", { + name: duplicateMatch[1].trim(), + }) + : rawErrorMessage; + return (
{activeError && ( @@ -363,14 +891,96 @@ function ProviderForm({ - {activeError instanceof Error - ? activeError.message - : String(activeError)} + {friendlyErrorMessage} )} + {presets.length > 0 && ( + + +
+ {t("providerPresetsTitle")} + + {t("providerPresetsDescription")} + +
+
+ + + +
+ )} +
@@ -559,7 +1169,28 @@ function ProviderForm({ fieldState.error, )} > - +
+ + {selectedPreset?.homepage && ( + + )} +
)} /> @@ -1009,11 +1642,8 @@ export default function InferenceProvidersPage() { ? { type: "detail" } : panel; - const selectedAgentKeys = useMemo(() => { - return resolvedPanel.type === "agent" - ? new Set([resolvedPanel.agentId]) - : new Set(); - }, [resolvedPanel]); + const selectedAgentKey = + resolvedPanel.type === "agent" ? resolvedPanel.agentId : null; const selectedProviderKeys = useMemo(() => { return activeProvider && @@ -1105,54 +1735,58 @@ export default function InferenceProvidersPage() { count={filteredCodingAgents.length} icon={} /> - {filteredCodingAgents.length === 0 ? ( -
-

- {searchQuery.trim() - ? t("noCodingAgentsMatch") - : t("noAgentsAvailable")} -

-
- ) : ( - { - if (keys === "all") return; - const agentId = [...keys][0] as - | CodingAgentId - | undefined; - if (!agentId) return; - handleAgentClick(agentId); - }} - className="p-2" - > - {filteredCodingAgents.map((agent) => ( - -
- -
- -
-
-
- ))} -
- )} +
+ {filteredCodingAgents.length === 0 ? ( +
+

+ {searchQuery.trim() + ? t("noCodingAgentsMatch") + : t("noAgentsAvailable")} +

+
+ ) : ( + + )} +
Date: Sun, 3 May 2026 20:07:32 +0800 Subject: [PATCH 37/62] feat(api): add inference provider preset DTO and bundled catalog InferenceProviderPresetResponse exposes id, name, base URL, format, model list, logo identifier, and optional homepage/description. JSON catalog seeded with OpenAI, Anthropic, OpenRouter, Groq, Mistral, Together, DeepSeek. Type registered with ts-rs export. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/api/src/bin/export-dto.rs | 8 +- .../dto/data/inference_provider_presets.json | 92 +++++++++++++++++++ crates/api/src/dto/inference.rs | 17 ++++ 3 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 crates/api/src/dto/data/inference_provider_presets.json diff --git a/crates/api/src/bin/export-dto.rs b/crates/api/src/bin/export-dto.rs index 33c1c5c2..40afc42f 100644 --- a/crates/api/src/bin/export-dto.rs +++ b/crates/api/src/bin/export-dto.rs @@ -20,9 +20,10 @@ use aghub_api::dto::{ CodexProfileResponse, CodexProviderStateResponse, CreateAgentProviderRequest, CreateInferenceProviderRequest, InferenceProviderFormatDto, InferenceProviderPasswordResponse, - InferenceProviderResponse, UpdateAgentProviderRequest, - UpdateClaudeProviderRequest, UpdateCodexActiveProfileRequest, - UpdateCodexProfileProviderRequest, UpdateInferenceProviderRequest, + InferenceProviderPresetResponse, InferenceProviderResponse, + UpdateAgentProviderRequest, UpdateClaudeProviderRequest, + UpdateCodexActiveProfileRequest, UpdateCodexProfileProviderRequest, + UpdateInferenceProviderRequest, }, integrations::{ CodeEditorType, EditSkillFolderRequest, OpenSkillFolderRequest, @@ -143,6 +144,7 @@ fn main() -> Result<(), Box> { export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; + export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; export_type::(&cfg)?; diff --git a/crates/api/src/dto/data/inference_provider_presets.json b/crates/api/src/dto/data/inference_provider_presets.json new file mode 100644 index 00000000..fa7d758d --- /dev/null +++ b/crates/api/src/dto/data/inference_provider_presets.json @@ -0,0 +1,92 @@ +[ + { + "id": "openai", + "name": "OpenAI", + "api_base_url": "https://api.openai.com/v1", + "format": "openai_responses", + "models": ["gpt-4o", "gpt-4o-mini", "o1-mini"], + "logo": "OpenAI", + "homepage": "https://platform.openai.com/api-keys", + "description": "Official OpenAI API." + }, + { + "id": "anthropic", + "name": "Anthropic", + "api_base_url": "https://api.anthropic.com", + "format": "anthropic", + "models": [ + "claude-3-5-sonnet-latest", + "claude-3-5-haiku-latest", + "claude-3-opus-latest" + ], + "logo": "Anthropic", + "homepage": "https://console.anthropic.com/settings/keys", + "description": "Claude models via the Anthropic Messages API." + }, + { + "id": "openrouter", + "name": "OpenRouter", + "api_base_url": "https://openrouter.ai/api/v1", + "format": "openai_completions", + "models": [ + "openai/gpt-4o-mini", + "anthropic/claude-3.5-sonnet", + "meta-llama/llama-3.1-70b-instruct" + ], + "logo": "OpenRouter", + "homepage": "https://openrouter.ai/keys", + "description": "Unified gateway for 100+ models." + }, + { + "id": "groq", + "name": "Groq", + "api_base_url": "https://api.groq.com/openai/v1", + "format": "openai_completions", + "models": [ + "llama-3.3-70b-versatile", + "llama-3.1-8b-instant", + "mixtral-8x7b-32768" + ], + "logo": "Groq", + "homepage": "https://console.groq.com/keys", + "description": "Low-latency inference on LPU hardware." + }, + { + "id": "mistral", + "name": "Mistral", + "api_base_url": "https://api.mistral.ai/v1", + "format": "openai_completions", + "models": [ + "mistral-large-latest", + "mistral-small-latest", + "codestral-latest" + ], + "logo": "Mistral", + "homepage": "https://console.mistral.ai/api-keys", + "description": "Mistral AI's hosted models." + }, + { + "id": "together", + "name": "Together", + "api_base_url": "https://api.together.xyz/v1", + "format": "openai_completions", + "models": [ + "meta-llama/Llama-3.3-70B-Instruct-Turbo", + "Qwen/Qwen2.5-Coder-32B-Instruct", + "deepseek-ai/DeepSeek-V3" + ], + "logo": "Together", + "homepage": "https://api.together.xyz/settings/api-keys", + "description": "Open-source model hosting." + }, + { + "id": "deepseek", + "name": "DeepSeek", + "api_base_url": "https://api.deepseek.com/v1", + "format": "openai_completions", + "models": ["deepseek-chat", "deepseek-reasoner"], + "logo": "DeepSeek", + "homepage": "https://platform.deepseek.com/api_keys", + "description": "DeepSeek's hosted Chat and Reasoner models." + } +] diff --git a/crates/api/src/dto/inference.rs b/crates/api/src/dto/inference.rs index 07b758b2..a258fcc4 100644 --- a/crates/api/src/dto/inference.rs +++ b/crates/api/src/dto/inference.rs @@ -131,6 +131,23 @@ pub struct InferenceProviderPasswordResponse { pub api_key: String, } +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct InferenceProviderPresetResponse { + pub id: String, + pub name: String, + pub api_base_url: String, + pub format: InferenceProviderFormatDto, + pub models: Vec, + pub logo: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub homepage: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, +} + #[derive(Debug, Clone, Copy, Serialize, TS)] #[ts(export)] #[serde(rename_all = "snake_case")] From f7e9a33a9158fe71d9ae6ca29434e5d33d261d57 Mon Sep 17 00:00:00 2001 From: danielchim Date: Sun, 3 May 2026 20:07:42 +0800 Subject: [PATCH 38/62] feat(api): expose GET /api/v1/inference/presets Returns the bundled provider preset catalog. Parsed once via OnceLock on first request. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/api/src/lib.rs | 1 + crates/api/src/routes/inference.rs | 25 ++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 78d0a73e..30b79cf4 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -167,6 +167,7 @@ pub async fn start(options: ApiOptions) -> Result<(), rocket::Error> { routes::credentials::create_credential, routes::credentials::delete_credential, routes::inference::list_inference_providers, + routes::inference::list_inference_provider_presets, routes::inference::list_opencode_providers, routes::inference::list_codex_providers, routes::inference::get_codex_state, diff --git a/crates/api/src/routes/inference.rs b/crates/api/src/routes/inference.rs index 2182982e..433edc59 100644 --- a/crates/api/src/routes/inference.rs +++ b/crates/api/src/routes/inference.rs @@ -12,9 +12,9 @@ use crate::dto::inference::{ AgentProviderResponse, ClaudeProviderStateResponse, CodexProviderStateResponse, CreateAgentProviderRequest, CreateInferenceProviderRequest, InferenceProviderPasswordResponse, - InferenceProviderResponse, UpdateAgentProviderRequest, - UpdateCodexActiveProfileRequest, UpdateCodexProfileProviderRequest, - UpdateInferenceProviderRequest, + InferenceProviderPresetResponse, InferenceProviderResponse, + UpdateAgentProviderRequest, UpdateCodexActiveProfileRequest, + UpdateCodexProfileProviderRequest, UpdateInferenceProviderRequest, }; use crate::error::{ApiCreated, ApiError, ApiNoContent, ApiResult}; use crate::state::InferenceProviderState; @@ -184,6 +184,25 @@ pub fn list_inference_providers( Ok(Json(providers)) } +const INFERENCE_PROVIDER_PRESETS_JSON: &str = + include_str!("../dto/data/inference_provider_presets.json"); + +fn inference_provider_presets() -> &'static [InferenceProviderPresetResponse] { + use std::sync::OnceLock; + static PRESETS: OnceLock> = + OnceLock::new(); + PRESETS.get_or_init(|| { + serde_json::from_str(INFERENCE_PROVIDER_PRESETS_JSON) + .expect("inference_provider_presets.json must be valid") + }) +} + +#[get("/inference/presets")] +pub fn list_inference_provider_presets( +) -> Json> { + Json(inference_provider_presets().to_vec()) +} + #[get("/inference/agents/opencode/providers")] pub fn list_opencode_providers( state: &State, From 8e5742d5578aa7555fc01d3a4a2fb9edff112785 Mon Sep 17 00:00:00 2001 From: danielchim Date: Sun, 3 May 2026 20:07:52 +0800 Subject: [PATCH 39/62] chore(desktop): regenerate DTOs for InferenceProviderPresetResponse Co-Authored-By: Claude Opus 4.7 (1M context) --- .../dto/InferenceProviderPresetResponse.ts | 13 +++++++++++++ crates/desktop/src/generated/dto/index.ts | 1 + 2 files changed, 14 insertions(+) create mode 100644 crates/desktop/src/generated/dto/InferenceProviderPresetResponse.ts diff --git a/crates/desktop/src/generated/dto/InferenceProviderPresetResponse.ts b/crates/desktop/src/generated/dto/InferenceProviderPresetResponse.ts new file mode 100644 index 00000000..e8af2eee --- /dev/null +++ b/crates/desktop/src/generated/dto/InferenceProviderPresetResponse.ts @@ -0,0 +1,13 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { InferenceProviderFormatDto } from "./InferenceProviderFormatDto"; + +export type InferenceProviderPresetResponse = { + id: string; + name: string; + api_base_url: string; + format: InferenceProviderFormatDto; + models: Array; + logo: string; + homepage?: string; + description?: string; +}; diff --git a/crates/desktop/src/generated/dto/index.ts b/crates/desktop/src/generated/dto/index.ts index d294385a..f6af8198 100644 --- a/crates/desktop/src/generated/dto/index.ts +++ b/crates/desktop/src/generated/dto/index.ts @@ -33,6 +33,7 @@ export type { GlobalSkillLockResponse } from "./GlobalSkillLockResponse"; export type { ImportSkillRequest } from "./ImportSkillRequest"; export type { InferenceProviderFormatDto } from "./InferenceProviderFormatDto"; export type { InferenceProviderPasswordResponse } from "./InferenceProviderPasswordResponse"; +export type { InferenceProviderPresetResponse } from "./InferenceProviderPresetResponse"; export type { InferenceProviderResponse } from "./InferenceProviderResponse"; export type { InstallScopeDto } from "./InstallScopeDto"; export type { InstallSkillRequest } from "./InstallSkillRequest"; From cb286e8477eb7e9f5da7ee982abf355076b8c02b Mon Sep 17 00:00:00 2001 From: danielchim Date: Sun, 3 May 2026 20:08:05 +0800 Subject: [PATCH 40/62] feat(desktop): add inference provider preset query and api client method api.inferenceProviders.listPresets() hits GET /inference/presets. inferenceProviderPresetsQueryOptions exposes a 1h staleTime so the catalog is effectively fetched once per app session and reused across form mounts. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/desktop/src/lib/api.ts | 4 ++++ .../src/requests/inference-providers.ts | 20 +++++++++++++++++++ crates/desktop/src/requests/keys.ts | 1 + 3 files changed, 25 insertions(+) diff --git a/crates/desktop/src/lib/api.ts b/crates/desktop/src/lib/api.ts index bc7feb8b..c4a1a88e 100644 --- a/crates/desktop/src/lib/api.ts +++ b/crates/desktop/src/lib/api.ts @@ -24,6 +24,7 @@ import type { GlobalSkillLockResponse, ImportSkillRequest, InferenceProviderPasswordResponse, + InferenceProviderPresetResponse, InferenceProviderResponse, InstallSkillRequest, InstallSkillResponse, @@ -490,6 +491,9 @@ export function createApi(baseUrl: string) { list(): Promise { return client.get("inference/providers").json(); }, + listPresets(): Promise { + return client.get("inference/presets").json(); + }, listOpenCode(): Promise { return client.get("inference/agents/opencode/providers").json(); }, diff --git a/crates/desktop/src/requests/inference-providers.ts b/crates/desktop/src/requests/inference-providers.ts index 2fad89b3..78e91970 100644 --- a/crates/desktop/src/requests/inference-providers.ts +++ b/crates/desktop/src/requests/inference-providers.ts @@ -8,6 +8,7 @@ import type { CodexProviderStateResponse, CreateAgentProviderRequest, CreateInferenceProviderRequest, + InferenceProviderPresetResponse, InferenceProviderResponse, UpdateAgentProviderRequest, UpdateCodexActiveProfileRequest, @@ -36,6 +37,25 @@ export function inferenceProviderListQueryOptions({ }); } +interface InferenceProviderPresetsQueryParams { + api: ApiClient; + enabled?: boolean; + staleTime?: number; +} + +export function inferenceProviderPresetsQueryOptions({ + api, + enabled = true, + staleTime = 60 * 60 * 1000, +}: InferenceProviderPresetsQueryParams) { + return queryOptions({ + queryKey: queryKeys.inferenceProviders.presets(), + queryFn: () => api.inferenceProviders.listPresets(), + enabled, + staleTime, + }); +} + export function openCodeProviderListQueryOptions({ api, enabled = true, diff --git a/crates/desktop/src/requests/keys.ts b/crates/desktop/src/requests/keys.ts index c5cae54d..839562cf 100644 --- a/crates/desktop/src/requests/keys.ts +++ b/crates/desktop/src/requests/keys.ts @@ -52,6 +52,7 @@ export const queryKeys = { inferenceProviders: { all: () => ["inference-providers"] as const, list: () => ["inference-providers", "list"] as const, + presets: () => ["inference-providers", "presets"] as const, agent: (agentId: string) => ["inference-providers", "agent", agentId] as const, agentState: (agentId: string) => From 50b2c360d0dd5a74cb485d6f5a57b0d8f41045b0 Mon Sep 17 00:00:00 2001 From: akarachen Date: Mon, 4 May 2026 00:38:38 +0800 Subject: [PATCH 41/62] fix: issue --- .../dto/data/inference_provider_presets.json | 24 +- crates/api/src/error.rs | 32 +- crates/api/src/routes/inference.rs | 13 +- crates/desktop/bun.lock | 1000 +---------------- crates/desktop/package.json | 2 +- .../desktop/src/pages/inference-providers.tsx | 53 +- .../inference-providers/claude-panel.tsx | 77 +- .../pages/inference-providers/codex-panel.tsx | 21 +- .../inference-providers/opencode-panel.tsx | 18 +- .../inference-providers/provider-selection.ts | 9 + .../src/requests/inference-providers.ts | 3 + .../0005_create_agent_provider_bindings.sql | 13 +- .../0006_drop_binding_is_active.sql | 13 +- crates/inference/src/claude/mod.rs | 17 +- crates/inference/src/codex/mapping.rs | 12 +- crates/inference/src/codex/mod.rs | 19 +- crates/inference/src/codex/tests.rs | 24 +- crates/inference/src/credentials.rs | 3 + crates/inference/src/opencode/mapping.rs | 2 + crates/inference/src/opencode/mod.rs | 10 +- crates/inference/src/opencode/tests.rs | 16 +- crates/inference/src/store.rs | 68 +- 22 files changed, 284 insertions(+), 1165 deletions(-) create mode 100644 crates/desktop/src/pages/inference-providers/provider-selection.ts diff --git a/crates/api/src/dto/data/inference_provider_presets.json b/crates/api/src/dto/data/inference_provider_presets.json index fa7d758d..bf33dffe 100644 --- a/crates/api/src/dto/data/inference_provider_presets.json +++ b/crates/api/src/dto/data/inference_provider_presets.json @@ -4,7 +4,7 @@ "name": "OpenAI", "api_base_url": "https://api.openai.com/v1", "format": "openai_responses", - "models": ["gpt-4o", "gpt-4o-mini", "o1-mini"], + "models": ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"], "logo": "OpenAI", "homepage": "https://platform.openai.com/api-keys", "description": "Official OpenAI API." @@ -14,11 +14,7 @@ "name": "Anthropic", "api_base_url": "https://api.anthropic.com", "format": "anthropic", - "models": [ - "claude-3-5-sonnet-latest", - "claude-3-5-haiku-latest", - "claude-3-opus-latest" - ], + "models": ["claude-opus-4-7", "claude-sonnet-4-6", "claude-haiku-4-5"], "logo": "Anthropic", "homepage": "https://console.anthropic.com/settings/keys", "description": "Claude models via the Anthropic Messages API." @@ -29,9 +25,9 @@ "api_base_url": "https://openrouter.ai/api/v1", "format": "openai_completions", "models": [ - "openai/gpt-4o-mini", - "anthropic/claude-3.5-sonnet", - "meta-llama/llama-3.1-70b-instruct" + "openai/gpt-5.5", + "anthropic/claude-sonnet-4.6", + "meta-llama/llama-4-maverick" ], "logo": "OpenRouter", "homepage": "https://openrouter.ai/keys", @@ -44,8 +40,8 @@ "format": "openai_completions", "models": [ "llama-3.3-70b-versatile", - "llama-3.1-8b-instant", - "mixtral-8x7b-32768" + "openai/gpt-oss-120b", + "llama-3.1-8b-instant" ], "logo": "Groq", "homepage": "https://console.groq.com/keys", @@ -72,8 +68,8 @@ "format": "openai_completions", "models": [ "meta-llama/Llama-3.3-70B-Instruct-Turbo", - "Qwen/Qwen2.5-Coder-32B-Instruct", - "deepseek-ai/DeepSeek-V3" + "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", + "deepseek-ai/DeepSeek-V4-Pro" ], "logo": "Together", "homepage": "https://api.together.xyz/settings/api-keys", @@ -84,7 +80,7 @@ "name": "DeepSeek", "api_base_url": "https://api.deepseek.com/v1", "format": "openai_completions", - "models": ["deepseek-chat", "deepseek-reasoner"], + "models": ["deepseek-v4-flash", "deepseek-v4-pro"], "logo": "DeepSeek", "homepage": "https://platform.deepseek.com/api_keys", "description": "DeepSeek's hosted Chat and Reasoner models." diff --git a/crates/api/src/error.rs b/crates/api/src/error.rs index 455c31cc..2dac0e5f 100644 --- a/crates/api/src/error.rs +++ b/crates/api/src/error.rs @@ -94,15 +94,29 @@ impl From for ApiError { | InferenceProviderError::InvalidFormat(_) | InferenceProviderError::UnsupportedAgentProviderCapability { .. - } - | InferenceProviderError::InvalidAgentProviderConfig { .. } - | InferenceProviderError::InvalidAgentCredentialStore { .. } => { - ApiError::new( - Status::BadRequest, - e.to_string(), - "INVALID_PARAM", - ) - } + } => ApiError::new( + Status::BadRequest, + e.to_string(), + "INVALID_PARAM", + ), + InferenceProviderError::InvalidAgentProviderConfig { + agent_id, + message, + .. + } => ApiError::new( + Status::BadRequest, + format!("invalid {agent_id} provider config: {message}"), + "INVALID_PARAM", + ), + InferenceProviderError::InvalidAgentCredentialStore { + agent_id, + message, + .. + } => ApiError::new( + Status::BadRequest, + format!("invalid {agent_id} credential store: {message}"), + "INVALID_PARAM", + ), InferenceProviderError::AlreadyExists(_) | InferenceProviderError::ModelAlreadyExists(_) => ApiError::new( Status::Conflict, diff --git a/crates/api/src/routes/inference.rs b/crates/api/src/routes/inference.rs index 433edc59..45d336d1 100644 --- a/crates/api/src/routes/inference.rs +++ b/crates/api/src/routes/inference.rs @@ -545,15 +545,10 @@ fn claude_state_response( .map(|binding| { // Built-in providers have no inventory backing; skip matching. let matched = match binding.source_provider_id.as_deref() { - Some(id) if !id.is_empty() => { - let agent_api_key = - store.get_api_key(id).map_err(ApiError::from)?; - find_matching_inventory_provider( - &inventory, - &binding, - agent_api_key, - )? - } + Some(id) if !id.is_empty() => inventory + .iter() + .find(|(provider, _)| provider.id == id) + .cloned(), _ => None, }; let response = AgentProviderResponse::from(binding); diff --git a/crates/desktop/bun.lock b/crates/desktop/bun.lock index e3adacff..1fe22815 100644 --- a/crates/desktop/bun.lock +++ b/crates/desktop/bun.lock @@ -8,7 +8,7 @@ "@heroicons/react": "^2.2.0", "@heroui/react": "^3.0.1", "@heroui/styles": "^3.0.1", - "@lobehub/icons": "^5.5.4", + "@lobehub/icons-static-svg": "^1.88.0", "@tanstack/react-query": "^5.94.5", "@tauri-apps/api": "^2", "@tauri-apps/plugin-deep-link": "^2.4.7", @@ -65,20 +65,6 @@ }, }, "packages": { - "@ant-design/colors": ["@ant-design/colors@8.0.1", "", { "dependencies": { "@ant-design/fast-color": "^3.0.0" } }, "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ=="], - - "@ant-design/cssinjs": ["@ant-design/cssinjs@2.1.2", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ=="], - - "@ant-design/cssinjs-utils": ["@ant-design/cssinjs-utils@2.1.2", "", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@babel/runtime": "^7.23.2", "@rc-component/util": "^1.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA=="], - - "@ant-design/fast-color": ["@ant-design/fast-color@3.0.1", "", {}, "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw=="], - - "@ant-design/icons": ["@ant-design/icons@6.1.1", "", { "dependencies": { "@ant-design/colors": "^8.0.0", "@ant-design/icons-svg": "^4.4.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-AMT4N2y++TZETNHiM77fs4a0uPVCJGuL5MTonk13Pvv7UN7sID1cNEZOc1qNqx6zLKAOilTEFAdAoAFKa0U//Q=="], - - "@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="], - - "@ant-design/react-slick": ["@ant-design/react-slick@2.0.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "clsx": "^2.1.1", "json2mq": "^0.2.0", "throttle-debounce": "^5.0.0" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg=="], - "@antfu/eslint-config": ["@antfu/eslint-config@8.1.1", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@clack/prompts": "^1.2.0", "@e18e/eslint-plugin": "^0.3.0", "@eslint-community/eslint-plugin-eslint-comments": "^4.7.1", "@eslint/markdown": "^8.0.1", "@stylistic/eslint-plugin": "^5.10.0", "@typescript-eslint/eslint-plugin": "^8.58.1", "@typescript-eslint/parser": "^8.58.1", "@vitest/eslint-plugin": "^1.6.14", "ansis": "^4.2.0", "cac": "^7.0.0", "eslint-config-flat-gitignore": "^2.3.0", "eslint-flat-config-utils": "^3.1.0", "eslint-merge-processors": "^2.0.0", "eslint-plugin-antfu": "^3.2.2", "eslint-plugin-command": "^3.5.2", "eslint-plugin-import-lite": "^0.6.0", "eslint-plugin-jsdoc": "^62.9.0", "eslint-plugin-jsonc": "^3.1.2", "eslint-plugin-n": "^17.24.0", "eslint-plugin-no-only-tests": "^3.3.0", "eslint-plugin-perfectionist": "^5.8.0", "eslint-plugin-pnpm": "^1.6.0", "eslint-plugin-regexp": "^3.1.0", "eslint-plugin-toml": "^1.3.1", "eslint-plugin-unicorn": "^64.0.0", "eslint-plugin-unused-imports": "^4.4.1", "eslint-plugin-vue": "^10.8.0", "eslint-plugin-yml": "^3.3.1", "eslint-processor-vue-blocks": "^2.0.0", "globals": "^17.4.0", "local-pkg": "^1.1.2", "parse-gitignore": "^2.0.0", "toml-eslint-parser": "^1.0.3", "vue-eslint-parser": "^10.4.0", "yaml-eslint-parser": "^2.0.0" }, "peerDependencies": { "@angular-eslint/eslint-plugin": "^21.1.0", "@angular-eslint/eslint-plugin-template": "^21.1.0", "@angular-eslint/template-parser": "^21.1.0", "@eslint-react/eslint-plugin": "^3.0.0", "@next/eslint-plugin-next": ">=15.0.0", "@prettier/plugin-xml": "^3.4.1", "@unocss/eslint-plugin": ">=0.50.0", "astro-eslint-parser": "^1.0.2", "eslint": "^9.10.0 || ^10.0.0", "eslint-plugin-astro": "^1.2.0", "eslint-plugin-format": ">=0.1.0", "eslint-plugin-jsx-a11y": ">=6.10.2", "eslint-plugin-react-refresh": "^0.5.0", "eslint-plugin-solid": "^0.14.3", "eslint-plugin-svelte": ">=2.35.1", "eslint-plugin-vuejs-accessibility": "^2.4.1", "prettier-plugin-astro": "^0.14.0", "prettier-plugin-slidev": "^1.0.5", "svelte-eslint-parser": ">=0.37.0" }, "optionalPeers": ["@angular-eslint/eslint-plugin", "@angular-eslint/eslint-plugin-template", "@angular-eslint/template-parser", "@eslint-react/eslint-plugin", "@next/eslint-plugin-next", "@prettier/plugin-xml", "@unocss/eslint-plugin", "astro-eslint-parser", "eslint-plugin-astro", "eslint-plugin-format", "eslint-plugin-jsx-a11y", "eslint-plugin-react-refresh", "eslint-plugin-solid", "eslint-plugin-svelte", "eslint-plugin-vuejs-accessibility", "prettier-plugin-astro", "prettier-plugin-slidev", "svelte-eslint-parser"], "bin": { "eslint-config": "bin/index.mjs" } }, "sha512-y5/eAKlJUbQpeES2Pnb0i/VgbmqQ+srHJJNqbTKEBsxdLy3h1BqdS00zDpE+YeP71EWmlYJSTUhcJg4n4yMeAQ=="], "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], @@ -119,36 +105,10 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@base-ui/react": ["@base-ui/react@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@base-ui/utils": "0.2.3", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "tabbable": "^6.3.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-4USBWz++DUSLTuIYpbYkSgy1F9ZmNG9S/lXvlUN6qMK0P0RlW+6eQmDUB4DgZ7HVvtXl4pvi4z5J2fv6Z3+9hg=="], - - "@base-ui/utils": ["@base-ui/utils@0.2.3", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-/CguQ2PDaOzeVOkllQR8nocJ0FFIDqsWIcURsVmm53QGo8NhFNpePjNlyPIB41luxfOqnG7PU0xicMEw3ls7XQ=="], - - "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], - - "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="], - - "@chevrotain/gast": ["@chevrotain/gast@12.0.0", "", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="], - - "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@12.0.0", "", {}, "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA=="], - - "@chevrotain/types": ["@chevrotain/types@12.0.0", "", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="], - - "@chevrotain/utils": ["@chevrotain/utils@12.0.0", "", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="], - "@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="], "@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="], - "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], - - "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], - - "@dnd-kit/modifiers": ["@dnd-kit/modifiers@9.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw=="], - - "@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="], - - "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], - "@e18e/eslint-plugin": ["@e18e/eslint-plugin@0.3.0", "", { "dependencies": { "eslint-plugin-depend": "^1.5.0" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0", "oxlint": "^1.55.0" }, "optionalPeers": ["eslint", "oxlint"] }, "sha512-hHgfpxsrZ2UYHcicA+tGZnmk19uJTaye9VH79O+XS8R4ona2Hx3xjhXghclNW58uXMk3xXlbYEOMr8thsoBmWg=="], "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], @@ -157,36 +117,6 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - "@emoji-mart/data": ["@emoji-mart/data@1.2.1", "", {}, "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw=="], - - "@emoji-mart/react": ["@emoji-mart/react@1.1.1", "", { "peerDependencies": { "emoji-mart": "^5.2", "react": "^16.8 || ^17 || ^18" } }, "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g=="], - - "@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="], - - "@emotion/cache": ["@emotion/cache@11.14.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="], - - "@emotion/css": ["@emotion/css@11.13.5", "", { "dependencies": { "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.13.5", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2" } }, "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w=="], - - "@emotion/hash": ["@emotion/hash@0.8.0", "", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="], - - "@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.4.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="], - - "@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], - - "@emotion/react": ["@emotion/react@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="], - - "@emotion/serialize": ["@emotion/serialize@1.3.3", "", { "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA=="], - - "@emotion/sheet": ["@emotion/sheet@1.4.0", "", {}, "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="], - - "@emotion/unitless": ["@emotion/unitless@0.7.5", "", {}, "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="], - - "@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.2.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg=="], - - "@emotion/utils": ["@emotion/utils@1.4.2", "", {}, "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="], - - "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="], - "@es-joy/jsdoccomment": ["@es-joy/jsdoccomment@0.84.0", "", { "dependencies": { "@types/estree": "^1.0.8", "@typescript-eslint/types": "^8.54.0", "comment-parser": "1.4.5", "esquery": "^1.7.0", "jsdoc-type-pratt-parser": "~7.1.1" } }, "sha512-0xew1CxOam0gV5OMjh2KjFQZsKL2bByX1+q4j3E73MpYIdyUxcZb/xQct9ccUb+ve5KGUYbCUxyPnYB7RbuP+w=="], "@es-joy/resolve.exports": ["@es-joy/resolve.exports@1.2.0", "", {}, "sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g=="], @@ -225,16 +155,6 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], - "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], - - "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], - - "@floating-ui/react": ["@floating-ui/react@0.27.19", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog=="], - - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], - - "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], - "@formatjs/ecma402-abstract": ["@formatjs/ecma402-abstract@2.3.6", "", { "dependencies": { "@formatjs/fast-memoize": "2.2.7", "@formatjs/intl-localematcher": "0.6.2", "decimal.js": "^10.4.3", "tslib": "^2.8.0" } }, "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw=="], "@formatjs/fast-memoize": ["@formatjs/fast-memoize@2.2.7", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ=="], @@ -245,8 +165,6 @@ "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="], - "@giscus/react": ["@giscus/react@3.1.0", "", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="], - "@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="], "@heroui/react": ["@heroui/react@3.0.2", "", { "dependencies": { "@heroui/styles": "3.0.2", "@radix-ui/react-avatar": "1.1.11", "@react-aria/i18n": "3.12.16", "@react-aria/ssr": "3.9.10", "@react-aria/utils": "3.33.1", "@react-stately/utils": "3.11.0", "@react-types/color": "3.1.4", "@react-types/shared": "3.33.1", "input-otp": "1.4.2", "react-aria-components": "1.16.0", "tailwind-merge": "3.4.0", "tailwind-variants": "3.2.2" }, "peerDependencies": { "react": ">=19.0.0", "react-dom": ">=19.0.0", "tailwindcss": ">=4.0.0" } }, "sha512-HWcYFurH+OnLITgIvQKyCd6BhYLApyzg0qqL3T5xemK5hgo1Nr+wQGQ5JSNVfBAmF4tWSS9TOzr24UHEO+21Ww=="], @@ -261,10 +179,6 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], - - "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], - "@internationalized/date": ["@internationalized/date@3.12.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ=="], "@internationalized/message": ["@internationalized/message@3.1.8", "", { "dependencies": { "@swc/helpers": "^0.5.0", "intl-messageformat": "^10.1.0" } }, "sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA=="], @@ -285,23 +199,7 @@ "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], - "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.5.1", "", {}, "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA=="], - - "@lit/reactive-element": ["@lit/reactive-element@2.1.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="], - - "@lobehub/emojilib": ["@lobehub/emojilib@1.0.0", "", {}, "sha512-s9KnjaPjsEefaNv150G3aifvB+J3P4eEKG+epY9zDPS2BeB6+V2jELWqAZll+nkogMaVovjEE813z3V751QwGw=="], - - "@lobehub/fluent-emoji": ["@lobehub/fluent-emoji@4.1.0", "", { "dependencies": { "@lobehub/emojilib": "^1.0.0", "antd-style": "^4.1.0", "emoji-regex": "^10.6.0", "es-toolkit": "^1.43.0", "lucide-react": "^0.562.0", "url-join": "^5.0.0" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-R1MB2lfUkDvB7XAQdRzY75c1dx/tB7gEvBPaEEMarzKfCJWmXm7rheS6caVzmgwAlq5sfmTbxPL+un99sp//Yw=="], - - "@lobehub/icons": ["@lobehub/icons@5.5.4", "", { "dependencies": { "antd-style": "^4.1.0", "es-toolkit": "^1.45.1", "lucide-react": "^0.469.0", "polished": "^4.3.1" }, "peerDependencies": { "@lobehub/ui": "^5.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-xxXGx/vQg1iXK6C/a2jlORCZycF7r46U5047kAdmYuvlczTC8PJ4WLPTjLk/0kG6DWjwFKHfCyP4ItM5dPOFbQ=="], - - "@lobehub/ui": ["@lobehub/ui@5.9.6", "", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@base-ui/react": "1.0.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@emotion/is-prop-valid": "^1.4.0", "@floating-ui/react": "^0.27.19", "@giscus/react": "^3.1.0", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@pierre/diffs": "^1.1.10", "@radix-ui/react-slot": "^1.2.4", "@shikijs/core": "^4.0.2", "@shikijs/transformers": "^4.0.2", "@splinetool/runtime": "0.9.526", "ahooks": "^3.9.7", "antd-style": "^4.1.0", "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.20", "emoji-mart": "^5.6.0", "es-toolkit": "^1.45.1", "fast-deep-equal": "^3.1.3", "immer": "^11.1.4", "katex": "^0.16.44", "leva": "^0.10.1", "lucide-react": "^1.7.0", "marked": "^17.0.5", "mermaid": "^11.14.0", "motion": "^12.38.0", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.3.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.12.0", "rc-input-number": "^9.5.0", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^15.1.0", "react-error-boundary": "^6.1.1", "react-hotkeys-hook": "^5.2.4", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.3", "react-zoom-pan-pinch": "^3.7.0", "rehype-github-alerts": "^4.2.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-cjk-friendly": "^2.0.1", "remark-gfm": "^4.0.1", "remark-github": "^12.0.0", "remark-math": "^6.0.0", "remend": "^1.3.0", "shiki": "^4.0.2", "shiki-stream": "^0.1.4", "swr": "^2.4.1", "ts-md5": "^2.0.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^13.0.0", "virtua": "^0.49.0" }, "peerDependencies": { "@lobehub/fluent-emoji": "^4.0.0", "@lobehub/icons": "^5.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-QHBAiJWZvSuFcLrkbigEDYGSNsXLvXattCASoZBVfoXMFWCbGgE1+5oIaboHjuSeKiemdss0OgIMnasa9y8EqQ=="], - - "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], - - "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], - - "@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="], + "@lobehub/icons-static-svg": ["@lobehub/icons-static-svg@1.88.0", "", {}, "sha512-46rnH5oNDr7OiBJ5s/T/1z8gs59E8hPpMhzSV3gi0KxziN8sJq+VIz7TA0BVduMGgv6p9BGk519Ig5vJXgK6hA=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], @@ -395,146 +293,24 @@ "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.19.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw=="], - "@pierre/diffs": ["@pierre/diffs@1.1.19", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-eYyDW69heXd7i9zdkWogGYosHzoYF2dstV6uDcmnQAf72uRChs3hrpf/7ym/ayTiwD8a+TQ7oZ5vNNb0tstJvA=="], - - "@pierre/theme": ["@pierre/theme@0.0.28", "", {}, "sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw=="], - "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], - "@primer/octicons": ["@primer/octicons@19.25.0", "", { "dependencies": { "object-assign": "^4.1.1" } }, "sha512-E0eMV8nXexrs7Vro7PdS8v/JfvvYCMh8HN6CXJ9l8fk9atZaY05fVUcyiAh5KjEJu7IxdFy4URfHGpM7+iOl1A=="], - - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], - - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], - "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], "@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], - - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], - - "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], - - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4kY9IVa6+9nJPsYmngK5Uk2kUmZnv7ChhHAFeQ5oaj8jrR1bIi3xww8nH71pz1/Ve4d/cXO3YxT8eikt1B0a8w=="], - - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], - "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], - - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], - - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], - "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="], "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], - - "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], - - "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], - - "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - - "@rc-component/async-validator": ["@rc-component/async-validator@5.1.0", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA=="], - - "@rc-component/cascader": ["@rc-component/cascader@1.14.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ=="], - - "@rc-component/checkbox": ["@rc-component/checkbox@2.0.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ=="], - - "@rc-component/collapse": ["@rc-component/collapse@1.2.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw=="], - - "@rc-component/color-picker": ["@rc-component/color-picker@3.1.1", "", { "dependencies": { "@ant-design/fast-color": "^3.0.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg=="], - - "@rc-component/context": ["@rc-component/context@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw=="], - - "@rc-component/dialog": ["@rc-component/dialog@1.8.4", "", { "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw=="], - - "@rc-component/drawer": ["@rc-component/drawer@1.4.2", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.1.3", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q=="], - - "@rc-component/dropdown": ["@rc-component/dropdown@1.0.2", "", { "dependencies": { "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg=="], - - "@rc-component/form": ["@rc-component/form@1.8.1", "", { "dependencies": { "@rc-component/async-validator": "^5.1.0", "@rc-component/util": "^1.6.2", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-8O7TB55Fi2mWIGvSnwZjk8jFqVNYyKDAswglwGShcbndxqzKz4cHwNtNaLjZlAeRge9wcB0LL8IWsC/Bl18raQ=="], - - "@rc-component/image": ["@rc-component/image@1.9.0", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ=="], - - "@rc-component/input": ["@rc-component/input@1.1.2", "", { "dependencies": { "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg=="], - - "@rc-component/input-number": ["@rc-component/input-number@1.6.2", "", { "dependencies": { "@rc-component/mini-decimal": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w=="], - - "@rc-component/mentions": ["@rc-component/mentions@1.6.0", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/menu": "~1.2.0", "@rc-component/textarea": "~1.1.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ=="], - - "@rc-component/menu": ["@rc-component/menu@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg=="], - - "@rc-component/mini-decimal": ["@rc-component/mini-decimal@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.18.0" } }, "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw=="], - - "@rc-component/motion": ["@rc-component/motion@1.3.2", "", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-itfd+GztzJYAb04Z4RkEub1TbJAfZc2Iuy8p44U44xD1F5+fNYFKI3897ijlbIyfvXkTmMm+KGcjkQQGMHywEQ=="], - - "@rc-component/mutate-observer": ["@rc-component/mutate-observer@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w=="], - - "@rc-component/notification": ["@rc-component/notification@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA=="], - - "@rc-component/overflow": ["@rc-component/overflow@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-syfmgAABaHCnCDzPwHZ/2tuvIcpOO3jefYZMmfkN+pmo8HKTzsfhS57vxo4ksPdN0By+uWVJhJWNFozNBxi2eA=="], - - "@rc-component/pagination": ["@rc-component/pagination@1.2.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw=="], - - "@rc-component/picker": ["@rc-component/picker@1.9.1", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g=="], - - "@rc-component/portal": ["@rc-component/portal@1.1.2", "", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg=="], - - "@rc-component/progress": ["@rc-component/progress@1.0.2", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ=="], - - "@rc-component/qrcode": ["@rc-component/qrcode@1.1.1", "", { "dependencies": { "@babel/runtime": "^7.24.7" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA=="], - - "@rc-component/rate": ["@rc-component/rate@1.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw=="], - - "@rc-component/resize-observer": ["@rc-component/resize-observer@1.1.2", "", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q=="], - - "@rc-component/segmented": ["@rc-component/segmented@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg=="], - - "@rc-component/select": ["@rc-component/select@1.6.15", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-SyVCWnqxCQZZcQvQJ/CxSjx2bGma6ds/HtnpkIfZVnt6RoEgbqUmHgD6vrzNarNXwbLXerwVzWwq8F3d1sst7g=="], - - "@rc-component/slider": ["@rc-component/slider@1.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g=="], - - "@rc-component/steps": ["@rc-component/steps@1.2.2", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw=="], - - "@rc-component/switch": ["@rc-component/switch@1.0.3", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw=="], - - "@rc-component/table": ["@rc-component/table@1.9.1", "", { "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.1.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg=="], - - "@rc-component/tabs": ["@rc-component/tabs@1.7.0", "", { "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w=="], - - "@rc-component/textarea": ["@rc-component/textarea@1.1.2", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A=="], - - "@rc-component/tooltip": ["@rc-component/tooltip@1.4.0", "", { "dependencies": { "@rc-component/trigger": "^3.7.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg=="], - - "@rc-component/tour": ["@rc-component/tour@2.3.0", "", { "dependencies": { "@rc-component/portal": "^2.2.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow=="], - - "@rc-component/tree": ["@rc-component/tree@1.2.4", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.8.1", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w=="], - - "@rc-component/tree-select": ["@rc-component/tree-select@1.8.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ=="], - - "@rc-component/trigger": ["@rc-component/trigger@3.9.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.2.0", "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg=="], - - "@rc-component/upload": ["@rc-component/upload@1.1.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw=="], - - "@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], - - "@rc-component/virtual-list": ["@rc-component/virtual-list@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ=="], - "@react-aria/autocomplete": ["@react-aria/autocomplete@3.0.0-rc.6", "", { "dependencies": { "@react-aria/combobox": "^3.15.0", "@react-aria/focus": "^3.21.5", "@react-aria/i18n": "^3.12.16", "@react-aria/interactions": "^3.27.1", "@react-aria/listbox": "^3.15.3", "@react-aria/searchfield": "^3.8.12", "@react-aria/textfield": "^3.18.5", "@react-aria/utils": "^3.33.1", "@react-stately/autocomplete": "3.0.0-beta.4", "@react-stately/combobox": "^3.13.0", "@react-types/autocomplete": "3.0.0-alpha.38", "@react-types/button": "^3.15.1", "@react-types/shared": "^3.33.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-uymUNJ8NW+dX7lmgkHE+SklAbxwktycAJcI5lBBw6KPZyc0EdMHC+/Fc5CUz3enIAhNwd2oxxogcSHknquMzQA=="], "@react-aria/breadcrumbs": ["@react-aria/breadcrumbs@3.5.32", "", { "dependencies": { "@react-aria/i18n": "^3.12.16", "@react-aria/link": "^3.8.9", "@react-aria/utils": "^3.33.1", "@react-types/breadcrumbs": "^3.7.19", "@react-types/shared": "^3.33.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-S61vh5DJ2PXiXUwD7gk+pvS/b4VPrc3ZJOUZ0yVRLHkVESr5LhIZH+SAVgZkm1lzKyMRG+BH+fiRH/DZRSs7SA=="], @@ -785,32 +561,10 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], - "@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="], - - "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="], - - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg=="], - - "@shikijs/langs": ["@shikijs/langs@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg=="], - - "@shikijs/primitive": ["@shikijs/primitive@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw=="], - - "@shikijs/themes": ["@shikijs/themes@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="], - - "@shikijs/transformers": ["@shikijs/transformers@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/types": "4.0.2" } }, "sha512-1+L0gf9v+SdDXs08vjaLb3mBFa8U7u37cwcBQIv/HCocLwX69Tt6LpUCjtB+UUTvQxI7BnjZKhN/wMjhHBcJGg=="], - - "@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], - - "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], - "@sindresorhus/base62": ["@sindresorhus/base62@1.0.0", "", {}, "sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA=="], - "@splinetool/runtime": ["@splinetool/runtime@0.9.526", "", { "dependencies": { "on-change": "^4.0.0", "semver-compare": "^1.0.0" } }, "sha512-qznHbXA5aKwDbCgESAothCNm1IeEZcmNWG145p5aXj4w5uoqR1TZ9qkTHTKLTsUbHeitCwdhzmRqan1kxboLgQ=="], - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], - "@stitches/react": ["@stitches/react@1.2.8", "", { "peerDependencies": { "react": ">= 16.3.0" } }, "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA=="], - "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.10.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/types": "^8.56.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0" } }, "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ=="], "@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], @@ -891,102 +645,28 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], - - "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], - - "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], - - "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], - - "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], - - "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], - - "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], - - "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], - - "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], - - "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], - - "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], - - "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], - - "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], - - "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], - - "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], - - "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], - - "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], - - "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], - - "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], - - "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], - - "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], - - "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], - - "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], - - "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], - - "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], - - "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], - - "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], - - "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], - - "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], - - "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], - - "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], - "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], - - "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - "@types/js-cookie": ["@types/js-cookie@3.0.6", "", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], - "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], - "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/type-utils": "8.58.1", "@typescript-eslint/utils": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ=="], @@ -1011,14 +691,6 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - - "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], - - "@use-gesture/core": ["@use-gesture/core@10.3.1", "", {}, "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw=="], - - "@use-gesture/react": ["@use-gesture/react@10.3.1", "", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="], - "@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], @@ -1039,8 +711,6 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "ahooks": ["ahooks@3.9.7", "", { "dependencies": { "@babel/runtime": "^7.21.0", "@types/js-cookie": "^3.0.6", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-S0lvzhbdlhK36RFBkGv+RbOM/dbbweym+BIHM/bwwuWVSVN5TuVErHPMWo4w0t1NDYg5KPp2iEf7Y7E5LASYiw=="], - "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -1049,24 +719,10 @@ "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], - "antd": ["antd@6.3.6", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", "@ant-design/icons": "^6.1.1", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.28.4", "@rc-component/cascader": "~1.14.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.8.4", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.8.0", "@rc-component/image": "~1.9.0", "@rc-component/input": "~1.1.2", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.6.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.3.2", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~1.2.0", "@rc-component/pagination": "~1.2.0", "@rc-component/picker": "~1.9.1", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.9.1", "@rc-component/tabs": "~1.7.0", "@rc-component/textarea": "~1.1.2", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.3.0", "@rc-component/tree": "~1.2.4", "@rc-component/tree-select": "~1.8.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-zdCYjusrTUn4gNxEg4PH8MWlfuXYbKfuGOkjgZ0Rg6DpWbIVmG/MwvsZ5yvG6z3Y6UI/gzYpaQ82iTt4KdbeaA=="], - - "antd-style": ["antd-style@4.1.0", "", { "dependencies": { "@ant-design/cssinjs": "^2.0.0", "@babel/runtime": "^7.24.1", "@emotion/cache": "^11.11.0", "@emotion/css": "^11.11.2", "@emotion/react": "^11.11.4", "@emotion/serialize": "^1.1.3", "@emotion/utils": "^1.2.1", "use-merge-value": "^1.2.0" }, "peerDependencies": { "antd": ">=6.0.0", "react": ">=18" } }, "sha512-vnPBGg0OVlSz90KRYZhxd89aZiOImTiesF+9MQqN8jsLGZUQTjbP04X9jTdEfsztKUuMbBWg/RmB/wHTakbtMQ=="], - "are-docs-informative": ["are-docs-informative@0.0.2", "", {}, "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig=="], - "assign-symbols": ["assign-symbols@1.0.0", "", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="], - - "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], - - "attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="], - - "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="], - "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], - "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.17", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA=="], @@ -1087,8 +743,6 @@ "cac": ["cac@7.0.0", "", {}, "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ=="], - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001787", "", {}, "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -1099,24 +753,8 @@ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], - "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], - - "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], - - "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - - "chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="], - - "chevrotain-allstar": ["chevrotain-allstar@0.4.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^12.0.0" } }, "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA=="], - - "chroma-js": ["chroma-js@3.2.0", "", {}, "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw=="], - "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], - "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], - - "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], - "clean-regexp": ["clean-regexp@1.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw=="], "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], @@ -1127,146 +765,50 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="], - - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "comment-parser": ["comment-parser@1.4.6", "", {}, "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg=="], "compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="], - "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], - "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "core-js-compat": ["core-js-compat@3.49.0", "", { "dependencies": { "browserslist": "^4.28.1" } }, "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA=="], - "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], - - "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "cytoscape": ["cytoscape@3.33.2", "", {}, "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw=="], - - "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], - - "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], - - "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], - - "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], - - "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], - - "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], - - "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], - - "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], - - "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], - - "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], - - "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], - - "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], - - "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], - - "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], - - "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], - - "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], - - "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], - - "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], - - "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], - - "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], - - "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], - - "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], - - "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], - - "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], - - "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], - - "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], - - "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], - - "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], - - "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], - - "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], - - "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], - - "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], - - "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], - - "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], - - "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], - - "dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], - "decode-uri-component": ["decode-uri-component@0.4.1", "", {}, "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], - "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], - "dompurify": ["dompurify@3.4.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw=="], - "dotenv": ["dotenv@17.4.1", "", {}, "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw=="], "driver.js": ["driver.js@1.4.0", "", {}, "sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew=="], "electron-to-chromium": ["electron-to-chromium@1.5.334", "", {}, "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog=="], - "emoji-mart": ["emoji-mart@5.6.0", "", {}, "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow=="], - "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], @@ -1275,16 +817,6 @@ "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-toolkit": ["es-toolkit@1.46.0", "", {}, "sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA=="], - - "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], - - "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -1367,28 +899,12 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], - - "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], - - "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - - "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], - - "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], - - "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], - "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - - "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -1413,14 +929,8 @@ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - "file-selector": ["file-selector@0.5.0", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "filter-obj": ["filter-obj@5.1.0", "", {}, "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng=="], - - "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], "find-up-simple": ["find-up-simple@1.0.1", "", {}, "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ=="], @@ -1429,18 +939,12 @@ "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], - "for-in": ["for-in@1.0.2", "", {}, "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ=="], - "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="], - "framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - "fuse.js": ["fuse.js@7.3.0", "", {}, "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], @@ -1449,10 +953,6 @@ "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], - "get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="], - - "giscus": ["giscus@1.6.0", "", { "dependencies": { "lit": "^3.2.1" } }, "sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ=="], - "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -1463,120 +963,48 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], - - "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], - - "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], - - "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="], - - "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], - - "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], - - "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], - - "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], - - "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], - - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], - - "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - - "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], - - "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], - - "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], - - "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], - "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], - "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], - "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], - - "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], - "i18next": ["i18next@26.0.4", "", { "dependencies": { "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-gXF7U9bfioXPLv7mw8Qt2nfO7vij5MyINvPgVv99pX3fL1Y01pw2mKBFrlYpRxRCl2wz3ISenj6VsMJT2isfuA=="], "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="], - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], - "input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="], - "internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], - - "intersection-observer": ["intersection-observer@0.12.2", "", {}, "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="], - "intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="], - "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], - - "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], - - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], - "is-builtin-module": ["is-builtin-module@5.0.0", "", { "dependencies": { "builtin-modules": "^5.0.0" } }, "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], - - "is-extendable": ["is-extendable@1.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4" } }, "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], - "is-mobile": ["is-mobile@5.0.0", "", {}, "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ=="], - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - - "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], @@ -1587,14 +1015,10 @@ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - "json2mq": ["json2mq@0.2.0", "", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="], - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "jsonc-eslint-parser": ["jsonc-eslint-parser@3.1.0", "", { "dependencies": { "acorn": "^8.5.0", "eslint-visitor-keys": "^5.0.0", "semver": "^7.3.5" } }, "sha512-75EA7EWZExL/j+MDKQrRbdzcRI2HOkRlmUw8fZJc1ioqFEOvBsq7Rt+A6yCxOt9w/TYNpkt52gC6nm/g5tFIng=="], @@ -1605,20 +1029,12 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], - "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "knip": ["knip@6.3.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "get-tsconfig": "4.13.7", "jiti": "^2.6.0", "minimist": "^1.2.8", "oxc-parser": "^0.121.0", "oxc-resolver": "^11.19.1", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.6.1", "strip-json-comments": "5.0.3", "unbash": "^2.2.0", "yaml": "^2.8.2", "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-22kLJloVcOVOAudCxlFOC0ICAMme7dKsS7pVTEnrmyKGpswb8ieznvAiSKUeFVDJhb01ect6dkDc1Ha1g1sPpg=="], "ky": ["ky@2.0.0", "", {}, "sha512-KzI4Vz5AbZFAUFYGx28PCSfFWUo6/qj9Br/P6KRwDieE1xfdz0tIONepJcLw/1xLocN13GgvfJGasa+pfSkbHg=="], - "langium": ["langium@4.2.2", "", { "dependencies": { "@chevrotain/regexp-to-ast": "~12.0.0", "chevrotain": "~12.0.0", "chevrotain-allstar": "~0.4.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ=="], - - "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], - - "leva": ["leva@0.10.1", "", { "dependencies": { "@radix-ui/react-portal": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.8", "@stitches/react": "^1.2.8", "@use-gesture/react": "^10.2.5", "colord": "^2.9.2", "dequal": "^2.0.2", "merge-value": "^1.0.0", "react-colorful": "^5.5.1", "react-dropzone": "^12.0.0", "v8n": "^1.3.3", "zustand": "^3.6.9" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-BcjnfUX8jpmwZUz2L7AfBtF9vn4ggTH33hmeufDULbP3YgNZ/C+ss/oO3stbrqRQyaOmRwy70y7BGTGO81S3rA=="], - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -1645,22 +1061,10 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "lit": ["lit@3.3.2", "", { "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", "lit-html": "^3.3.0" } }, "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ=="], - - "lit-element": ["lit-element@4.2.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0", "@lit/reactive-element": "^2.1.0", "lit-html": "^3.3.0" } }, "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w=="], - - "lit-html": ["lit-html@3.3.2", "", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw=="], - "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], - - "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], - "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], @@ -1669,22 +1073,12 @@ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], - - "lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], - "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - "marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], - "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], @@ -1705,40 +1099,20 @@ "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], - "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], - - "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], - - "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], - - "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], - - "mdast-util-newline-to-break": ["mdast-util-newline-to-break@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0" } }, "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog=="], - "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], - "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], - "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], - "merge-value": ["merge-value@1.0.0", "", { "dependencies": { "get-value": "^2.0.6", "is-extendable": "^1.0.0", "mixin-deep": "^1.2.0", "set-value": "^2.0.0" } }, "sha512-fJMmvat4NeKz63Uv9iHWcPDjCWcCkoiRoajRTEO8hlhUC6rwaHg0QCF9hBOTjZmm4JuglPckPSTtcuJL5kp0TQ=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "mermaid": ["mermaid@11.14.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g=="], - "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], - "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@2.0.1", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw=="], - - "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@3.0.1", "", { "dependencies": { "get-east-asian-width": "^1.4.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w=="], - "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], @@ -1757,22 +1131,10 @@ "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], - "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], - - "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], - - "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], - - "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], - - "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], - "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], - "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], - "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], @@ -1793,8 +1155,6 @@ "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], - "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], - "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], @@ -1819,18 +1179,10 @@ "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], - "mixin-deep": ["mixin-deep@1.3.2", "", { "dependencies": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" } }, "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA=="], - "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], "module-replacements": ["module-replacements@2.11.0", "", {}, "sha512-j5sNQm3VCpQQ7nTqGeOZtoJtV3uKERgCBm9QRhmGRiXiqkf7iRFOkfxdJRZWLkqYY8PNf4cDQF/WfXUYLENrRA=="], - "motion": ["motion@12.38.0", "", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="], - - "motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="], - - "motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nano-staged": ["nano-staged@1.0.2", "", { "bin": { "nano-staged": "lib/bin.js" } }, "sha512-Fytar3zHLY99nlMfqPPbraxZodqQAHPpdPRyYaplL+lB9DCR6pUrafxbG+Btz4+7fO5Rm/+DO4ZeDO/nLSUMhw=="], @@ -1845,22 +1197,12 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "numeral": ["numeral@2.0.6", "", {}, "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA=="], - "nuqs": ["nuqs@2.8.9", "", { "dependencies": { "@standard-schema/spec": "1.0.0" }, "peerDependencies": { "@remix-run/react": ">=2", "@tanstack/react-router": "^1", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router": "^5 || ^6 || ^7", "react-router-dom": "^5 || ^6 || ^7" }, "optionalPeers": ["@remix-run/react", "@tanstack/react-router", "next", "react-router", "react-router-dom"] }, "sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "object-deep-merge": ["object-deep-merge@2.0.0", "", {}, "sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg=="], - "on-change": ["on-change@4.0.2", "", {}, "sha512-cMtCyuJmTx/bg2HCpHo3ZLeF7FZnBOapLqZHr2AlLeJ5Ul0Zu2mUJJz051Fdwu/Et2YW04ZD+TtU+gVy0ACNCA=="], - "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], - - "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], @@ -1875,30 +1217,16 @@ "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - - "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - "parse-gitignore": ["parse-gitignore@2.0.0", "", {}, "sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog=="], "parse-imports-exports": ["parse-imports-exports@0.2.4", "", { "dependencies": { "parse-statements": "1.0.11" } }, "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ=="], - "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], - "parse-statements": ["parse-statements@1.0.11", "", {}, "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA=="], - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - - "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], @@ -1913,12 +1241,6 @@ "pnpm-workspace-yaml": ["pnpm-workspace-yaml@1.6.0", "", { "dependencies": { "yaml": "^2.8.2" } }, "sha512-uUy4dK3E11sp7nK+hnT7uAWfkBMe00KaUw8OG3NuNlYQoTk4sc9pcdIy1+XIP85v9Tvr02mK3JPaNNrP0QyRaw=="], - "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], - - "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], - - "polished": ["polished@4.3.1", "", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA=="], - "postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="], "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], @@ -1929,102 +1251,34 @@ "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], - - "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], - "query-string": ["query-string@9.3.1", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="], - "rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="], - - "rc-dialog": ["rc-dialog@9.6.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg=="], - - "rc-footer": ["rc-footer@0.6.8", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-JBZ+xcb6kkex8XnBd4VHw1ZxjV6kmcwUumSHaIFdka2qzMCo7Klcy4sI6G0XtUpG/vtpislQCc+S9Bc+NLHYMg=="], - - "rc-image": ["rc-image@7.12.0", "", { "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/portal": "^1.0.2", "classnames": "^2.2.6", "rc-dialog": "~9.6.0", "rc-motion": "^2.6.2", "rc-util": "^5.34.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q=="], - - "rc-input": ["rc-input@1.8.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.18.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA=="], - - "rc-input-number": ["rc-input-number@9.5.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/mini-decimal": "^1.0.1", "classnames": "^2.2.5", "rc-input": "~1.8.0", "rc-util": "^5.40.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag=="], - - "rc-menu": ["rc-menu@9.16.1", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^2.0.0", "classnames": "2.x", "rc-motion": "^2.4.3", "rc-overflow": "^1.3.1", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg=="], - - "rc-motion": ["rc-motion@2.9.5", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA=="], - - "rc-overflow": ["rc-overflow@1.5.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-resize-observer": "^1.0.0", "rc-util": "^5.37.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg=="], - - "rc-resize-observer": ["rc-resize-observer@1.4.3", "", { "dependencies": { "@babel/runtime": "^7.20.7", "classnames": "^2.2.1", "rc-util": "^5.44.1", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ=="], - - "rc-util": ["rc-util@5.44.4", "", { "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w=="], - - "re-resizable": ["re-resizable@6.11.2", "", { "peerDependencies": { "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A=="], - "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], "react-aria": ["react-aria@3.47.0", "", { "dependencies": { "@internationalized/string": "^3.2.7", "@react-aria/breadcrumbs": "^3.5.32", "@react-aria/button": "^3.14.5", "@react-aria/calendar": "^3.9.5", "@react-aria/checkbox": "^3.16.5", "@react-aria/color": "^3.1.5", "@react-aria/combobox": "^3.15.0", "@react-aria/datepicker": "^3.16.1", "@react-aria/dialog": "^3.5.34", "@react-aria/disclosure": "^3.1.3", "@react-aria/dnd": "^3.11.6", "@react-aria/focus": "^3.21.5", "@react-aria/gridlist": "^3.14.4", "@react-aria/i18n": "^3.12.16", "@react-aria/interactions": "^3.27.1", "@react-aria/label": "^3.7.25", "@react-aria/landmark": "^3.0.10", "@react-aria/link": "^3.8.9", "@react-aria/listbox": "^3.15.3", "@react-aria/menu": "^3.21.0", "@react-aria/meter": "^3.4.30", "@react-aria/numberfield": "^3.12.5", "@react-aria/overlays": "^3.31.2", "@react-aria/progress": "^3.4.30", "@react-aria/radio": "^3.12.5", "@react-aria/searchfield": "^3.8.12", "@react-aria/select": "^3.17.3", "@react-aria/selection": "^3.27.2", "@react-aria/separator": "^3.4.16", "@react-aria/slider": "^3.8.5", "@react-aria/ssr": "^3.9.10", "@react-aria/switch": "^3.7.11", "@react-aria/table": "^3.17.11", "@react-aria/tabs": "^3.11.1", "@react-aria/tag": "^3.8.1", "@react-aria/textfield": "^3.18.5", "@react-aria/toast": "^3.0.11", "@react-aria/tooltip": "^3.9.2", "@react-aria/tree": "^3.1.7", "@react-aria/utils": "^3.33.1", "@react-aria/visually-hidden": "^3.8.31", "@react-types/shared": "^3.33.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-nvahimIqdByl/PXk/xPkG30LPRzcin+/Uk0uFfwbbKRRFC9aa22a6BRULZLqVHwa9GaNyKe6CDUxO1Dde4v0kA=="], "react-aria-components": ["react-aria-components@1.16.0", "", { "dependencies": { "@internationalized/date": "^3.12.0", "@internationalized/string": "^3.2.7", "@react-aria/autocomplete": "3.0.0-rc.6", "@react-aria/collections": "^3.0.3", "@react-aria/dnd": "^3.11.6", "@react-aria/focus": "^3.21.5", "@react-aria/interactions": "^3.27.1", "@react-aria/live-announcer": "^3.4.4", "@react-aria/overlays": "^3.31.2", "@react-aria/ssr": "^3.9.10", "@react-aria/textfield": "^3.18.5", "@react-aria/toolbar": "3.0.0-beta.24", "@react-aria/utils": "^3.33.1", "@react-aria/virtualizer": "^4.1.13", "@react-stately/autocomplete": "3.0.0-beta.4", "@react-stately/layout": "^4.6.0", "@react-stately/selection": "^3.20.9", "@react-stately/table": "^3.15.4", "@react-stately/utils": "^3.11.0", "@react-stately/virtualizer": "^4.4.6", "@react-types/form": "^3.7.18", "@react-types/grid": "^3.3.8", "@react-types/shared": "^3.33.1", "@react-types/table": "^3.13.6", "@swc/helpers": "^0.5.0", "client-only": "^0.0.1", "react-aria": "^3.47.0", "react-stately": "^3.45.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-MjHbTLpMFzzD2Tv5KbeXoZwPczuUWZcRavVvQQlNHRtXHH38D+sToMEYpNeir7Wh3K/XWtzeX3EujfJW6QNkrw=="], - "react-avatar-editor": ["react-avatar-editor@15.1.0", "", { "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Zto7u9l6Wd5LPPtjeFJ+7uwoT4bs01OSgkN2kxD18lWl8IiZ0GY3nWCbKPx4qIU7Au1vENsMJm19rfVWHHayaQ=="], - - "react-colorful": ["react-colorful@5.6.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw=="], - "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], - "react-draggable": ["react-draggable@4.5.0", "", { "dependencies": { "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw=="], - - "react-dropzone": ["react-dropzone@12.1.0", "", { "dependencies": { "attr-accept": "^2.2.2", "file-selector": "^0.5.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8" } }, "sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog=="], - - "react-error-boundary": ["react-error-boundary@6.1.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w=="], - - "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], - "react-grab": ["react-grab@0.1.31", "", { "dependencies": { "@react-grab/cli": "0.1.31", "bippy": "^0.5.39" }, "peerDependencies": { "react": ">=17.0.0" }, "optionalPeers": ["react"], "bin": { "react-grab": "bin/cli.js" } }, "sha512-JAdlg46rNFv58l0tGs6omroDlCo1+oj70v03tyaP5AOHbx1wNNP1aaoTcDLSlcN4K5gwve9zR8/t0CT2mwLqSA=="], "react-hook-form": ["react-hook-form@7.72.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig=="], - "react-hotkeys-hook": ["react-hotkeys-hook@5.2.4", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-BgKg+A1+TawkYluh5Bo4cTmcgMN5L29uhJbDUQdHwPX+qgXRjIPYU5kIDHyxnAwCkCBiu9V5OpB2mpyeluVF2A=="], - "react-i18next": ["react-i18next@17.0.2", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.0.1", "react": ">= 16.8.0", "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA=="], - "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], - - "react-merge-refs": ["react-merge-refs@3.0.2", "", { "peerDependencies": { "react": ">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["react"] }, "sha512-MSZAfwFfdbEvwkKWP5EI5chuLYnNUxNS7vyS0i1Jp+wtd8J4Ga2ddzhaE68aMol2Z4vCnRM/oGOo1a3V75UPlw=="], - - "react-rnd": ["react-rnd@10.5.3", "", { "dependencies": { "re-resizable": "^6.11.2", "react-draggable": "^4.5.0", "tslib": "2.6.2" }, "peerDependencies": { "react": ">=16.3.0", "react-dom": ">=16.3.0" } }, "sha512-s/sIT3pGZnQ+57egijkTp9mizjIWrJz68Pq6yd+F/wniFY3IriML18dUXnQe/HP9uMiJ+9MAp44hljG99fZu6Q=="], - "react-stately": ["react-stately@3.45.0", "", { "dependencies": { "@react-stately/calendar": "^3.9.3", "@react-stately/checkbox": "^3.7.5", "@react-stately/collections": "^3.12.10", "@react-stately/color": "^3.9.5", "@react-stately/combobox": "^3.13.0", "@react-stately/data": "^3.15.2", "@react-stately/datepicker": "^3.16.1", "@react-stately/disclosure": "^3.0.11", "@react-stately/dnd": "^3.7.4", "@react-stately/form": "^3.2.4", "@react-stately/list": "^3.13.4", "@react-stately/menu": "^3.9.11", "@react-stately/numberfield": "^3.11.0", "@react-stately/overlays": "^3.6.23", "@react-stately/radio": "^3.11.5", "@react-stately/searchfield": "^3.5.19", "@react-stately/select": "^3.9.2", "@react-stately/selection": "^3.20.9", "@react-stately/slider": "^3.7.5", "@react-stately/table": "^3.15.4", "@react-stately/tabs": "^3.8.9", "@react-stately/toast": "^3.1.3", "@react-stately/toggle": "^3.9.5", "@react-stately/tooltip": "^3.5.11", "@react-stately/tree": "^3.9.6", "@react-types/shared": "^3.33.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-G3bYr0BIiookpt4H05VeZUuVS/FslQAj2TeT8vDfCiL314Y+LtPXIPe/a3eamCA0wljy7z1EDYKV50Qbz7pcJg=="], "react-virtuoso": ["react-virtuoso@4.18.4", "", { "peerDependencies": { "react": ">=16 || >=17 || >= 18 || >= 19", "react-dom": ">=16 || >=17 || >= 18 || >=19" } }, "sha512-DNM4Wy2tMA/J6ejMaDdqecOug31rOwgSRg4C/Dw6Iox4dJe9qwcx32M8HdhkE5uHEVVZh7h0koYwAsCSNdxGfQ=="], - "react-zoom-pan-pinch": ["react-zoom-pan-pinch@3.7.0", "", { "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA=="], - - "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], - - "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], - - "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], - - "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], - "refa": ["refa@0.12.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.8.0" } }, "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g=="], - "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], - - "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], - - "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], - "regexp-ast-analysis": ["regexp-ast-analysis@0.7.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.8.0", "refa": "^0.12.1" } }, "sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A=="], "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], @@ -2033,86 +1287,30 @@ "regjsparser": ["regjsparser@0.13.1", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw=="], - "rehype-github-alerts": ["rehype-github-alerts@4.2.0", "", { "dependencies": { "@primer/octicons": "^19.20.0", "hast-util-from-html": "^2.0.3", "hast-util-is-element": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-6di6kEu9WUHKLKrkKG2xX6AOuaCMGghg0Wq7MEuM/jBYUPVIq6PJpMe00dxMfU+/YSBtDXhffpDimgDi+BObIQ=="], - - "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], - - "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], - - "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], - - "remark-breaks": ["remark-breaks@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="], - - "remark-cjk-friendly": ["remark-cjk-friendly@2.0.1", "", { "dependencies": { "micromark-extension-cjk-friendly": "2.0.1" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-6WwkoQyZf/4j5k53zdFYrR8Ca+UVn992jXdLUSBDZR4eBpFhKyVxmA4gUHra/5fesjGIxrDhHesNr/sVoiiysA=="], - - "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], - - "remark-github": ["remark-github@12.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0", "mdast-util-to-string": "^4.0.0", "to-vfile": "^8.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-ByefQKFN184LeiGRCabfl7zUJsdlMYWEhiLX1gpmQ11yFg6xSuOTW7LVCv0oc1x+YvUMJW23NU36sJX2RWGgvg=="], - - "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], - - "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], - - "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], - - "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], - - "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], - - "remend": ["remend@1.3.0", "", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="], - - "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], - "reserved-identifiers": ["reserved-identifiers@1.2.0", "", {}, "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw=="], - "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="], - - "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], - "rolldown": ["rolldown@1.0.0-rc.15", "", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="], "rooks": ["rooks@9.8.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash.debounce": "^4.0.8", "raf": "^3.4.1", "use-sync-external-store": "^1.6.0" }, "optionalDependencies": { "@js-temporal/polyfill": "^0.5.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-S6FqnmERx5zgl8ZUEcnyTe1jgjwE5xeFCgOV4bzgQHKp26P7YA7uPnzzOgacojtoX6E7pQTcewSGqN83wVyz+g=="], - "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "screenfull": ["screenfull@5.2.0", "", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="], - - "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], - "scslre": ["scslre@0.3.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.8.0", "refa": "^0.12.0", "regexp-ast-analysis": "^0.7.0" } }, "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ=="], "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], - - "set-value": ["set-value@2.0.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", "is-plain-object": "^2.0.3", "split-string": "^3.0.1" } }, "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="], - - "shiki-stream": ["shiki-stream@0.1.4", "", { "dependencies": { "@shikijs/core": "^3.0.0" }, "peerDependencies": { "react": "^19.0.0", "solid-js": "^1.9.0", "vue": "^3.2.0" }, "optionalPeers": ["react", "solid-js", "vue"] }, "sha512-4pz6JGSDmVTTkPJ/ueixHkFAXY4ySCc+unvCaDZV7hqq/sdJZirRxgIXSuNSKgiFlGTgRR97sdu2R8K55sPsrw=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "simple-icons": ["simple-icons@16.15.0", "", {}, "sha512-hOyY4Cdvh1D/FJa1Qx4nTvypCT2BoI3jpc4xjxVgwVh1Hmd9mnqBqBTziDytCj2f5UOAXCfdnwODiNv710aqkQ=="], @@ -2121,34 +1319,22 @@ "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], "spdx-expression-parse": ["spdx-expression-parse@4.0.0", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ=="], "spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="], - "split-on-first": ["split-on-first@3.0.0", "", {}, "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA=="], - - "split-string": ["split-string@3.1.0", "", { "dependencies": { "extend-shallow": "^3.0.0" } }, "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw=="], - "stable-hash": ["stable-hash@0.0.6", "", {}, "sha512-0afH4mobqTybYZsXImQRLOjHV4gvOW+92HdUIax9t7a8d9v54KWykEuMVIcXhD9BCi+w3kS4x7O6fmZQ3JlG/g=="], "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], - "string-convert": ["string-convert@0.2.1", "", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="], - "string-ts": ["string-ts@2.3.1", "", {}, "sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], @@ -2157,22 +1343,10 @@ "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], - "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], - - "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], - - "stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "swr": ["swr@2.4.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="], - "synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="], - "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], - "tailwind-csstree": ["tailwind-csstree@0.3.0", "", { "peerDependencies": { "@eslint/css": ">=1.0.0" }, "optionalPeers": ["@eslint/css"] }, "sha512-FJmLCkH1ZDTEqJRVxMVhdiEbk/W67exqvDYrIQ759jsfv3mp3Gp/rrlgtr7dD5q7BK/t8LfaNqXM+BN6BrTKGA=="], "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], @@ -2183,8 +1357,6 @@ "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], - "throttle-debounce": ["throttle-debounce@5.0.2", "", {}, "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="], - "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], @@ -2193,22 +1365,12 @@ "to-valid-identifier": ["to-valid-identifier@1.0.0", "", { "dependencies": { "@sindresorhus/base62": "^1.0.0", "reserved-identifiers": "^1.0.0" } }, "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw=="], - "to-vfile": ["to-vfile@8.0.0", "", { "dependencies": { "vfile": "^6.0.0" } }, "sha512-IcmH1xB5576MJc9qcfEC/m/nQCFt3fzMHz45sSlgJyTWjRbKW1HAkJpuf3DgE57YzIlZcwcBZA5ENQbBo4aLkg=="], - "toml-eslint-parser": ["toml-eslint-parser@1.0.3", "", { "dependencies": { "eslint-visitor-keys": "^5.0.0" } }, "sha512-A5F0cM6+mDleacLIEUkmfpkBbnHJFV1d2rprHU2MXNk7mlxHq2zGojA+SRvQD1RoMo9gqjZPWEaKG4v1BQ48lw=="], - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - - "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], - "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "ts-declaration-location": ["ts-declaration-location@1.0.7", "", { "dependencies": { "picomatch": "^4.0.2" }, "peerDependencies": { "typescript": ">=4.0.0" } }, "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA=="], - "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], - - "ts-md5": ["ts-md5@2.0.1", "", {}, "sha512-yF35FCoEOFBzOclSkMNEUbFQZuv89KEQ+5Xz03HrMSGUGB1+r+El+JiGOFwsP4p9RFNzwlrydYoTLvPOuICl9w=="], - "ts-pattern": ["ts-pattern@5.9.0", "", {}, "sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg=="], "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], @@ -2229,16 +1391,8 @@ "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], - - "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], - "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], - "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], - - "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], - "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], @@ -2251,50 +1405,20 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="], - - "use-merge-value": ["use-merge-value@1.2.0", "", { "peerDependencies": { "react": ">= 16.x" } }, "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw=="], - "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], - - "v8n": ["v8n@1.5.1", "", {}, "sha512-LdabyT4OffkyXFCe9UT+uMkxNBs5rcTVuZClvxQr08D5TUgo1OFKkoT65qYRCsiKBl/usHjpXvP4hHMzzDRj3A=="], - "valibot": ["valibot@1.3.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg=="], - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], - - "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], - - "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - - "virtua": ["virtua@0.49.1", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-6f79msqg3jzNFdqJiS0FSzhRN1EHlDhR7EvW7emp6z5qQ22VdsReiDHflkpMEMhoAyUuYr69nwT0aagiM7NrUg=="], - "vite": ["vite@8.0.8", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], - "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], - - "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], - - "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], - - "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], - - "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], - - "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], - "vue-eslint-parser": ["vue-eslint-parser@10.4.0", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0 || ^9.0.0", "eslint-visitor-keys": "^4.2.0 || ^5.0.0", "espree": "^10.3.0 || ^11.0.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg=="], "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], @@ -2315,28 +1439,12 @@ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - "zustand": ["zustand@3.7.2", "", { "peerDependencies": { "react": ">=16.8" }, "optionalPeers": ["react"] }, "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="], - "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@emotion/babel-plugin/@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], - - "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], - - "@emotion/babel-plugin/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], - - "@emotion/babel-plugin/stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], - - "@emotion/cache/stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], - - "@emotion/serialize/@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], - - "@emotion/serialize/@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], - "@es-joy/jsdoccomment/comment-parser": ["comment-parser@1.4.5", "", {}, "sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw=="], "@es-joy/jsdoccomment/jsdoc-type-pratt-parser": ["jsdoc-type-pratt-parser@7.1.1", "", {}, "sha512-/2uqY7x6bsrpi3i9LVU6J89352C0rpMk0as8trXxCtvd4kPk1ke/Eyif6wqfSLvoNJqcDG9Vk4UsXgygzCt2xA=="], @@ -2349,44 +1457,6 @@ "@heroui/react/tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], - "@lobehub/fluent-emoji/lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], - - "@lobehub/ui/lucide-react": ["lucide-react@1.11.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g=="], - - "@mdx-js/mdx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="], - - "@pierre/diffs/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], - - "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-tooltip/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], - - "@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@rc-component/dialog/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], - - "@rc-component/drawer/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], - - "@rc-component/image/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], - - "@rc-component/tour/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], - - "@rc-component/trigger/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], - "@react-grab/cli/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@stylistic/eslint-plugin/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], @@ -2409,16 +1479,6 @@ "clean-regexp/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - "cosmiconfig/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], - - "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], - - "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], - - "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], - - "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "eslint-plugin-jsdoc/@es-joy/jsdoccomment": ["@es-joy/jsdoccomment@0.86.0", "", { "dependencies": { "@types/estree": "^1.0.8", "@typescript-eslint/types": "^8.58.0", "comment-parser": "1.4.6", "esquery": "^1.7.0", "jsdoc-type-pratt-parser": "~7.2.0" } }, "sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw=="], "eslint-plugin-jsonc/@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="], @@ -2431,14 +1491,8 @@ "eslint-plugin-yml/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "estree-util-build-jsx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "extend-shallow/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -2449,66 +1503,16 @@ "mdast-util-frontmatter/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], - - "mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], - "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - - "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - - "rc-menu/@rc-component/trigger": ["@rc-component/trigger@2.3.1", "", { "dependencies": { "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", "classnames": "^2.3.2", "rc-motion": "^2.0.0", "rc-resize-observer": "^1.3.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A=="], - - "react-rnd/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="], - "rolldown/@oxc-project/types": ["@oxc-project/types@0.124.0", "", {}, "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg=="], "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="], - "set-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], - - "shiki-stream/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - - "split-string/extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="], - - "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - - "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - - "@pierre/diffs/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - - "@pierre/diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], - - "@pierre/diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], - - "@pierre/diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], - - "@pierre/diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], - - "@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - - "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], - - "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - - "shiki-stream/@shikijs/core/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], } } diff --git a/crates/desktop/package.json b/crates/desktop/package.json index e6ef1246..231f075c 100644 --- a/crates/desktop/package.json +++ b/crates/desktop/package.json @@ -18,7 +18,7 @@ "@heroicons/react": "^2.2.0", "@heroui/react": "^3.0.1", "@heroui/styles": "^3.0.1", - "@lobehub/icons": "^5.5.4", + "@lobehub/icons-static-svg": "^1.88.0", "@tanstack/react-query": "^5.94.5", "@tauri-apps/api": "^2", "@tauri-apps/plugin-deep-link": "^2.4.7", diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index a77503ad..cc695616 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -10,13 +10,13 @@ import { ServerIcon, TrashIcon, } from "@heroicons/react/24/solid"; -import AnthropicIcon from "@lobehub/icons/es/Anthropic"; -import DeepSeekIcon from "@lobehub/icons/es/DeepSeek"; -import GroqIcon from "@lobehub/icons/es/Groq"; -import MistralIcon from "@lobehub/icons/es/Mistral"; -import OpenAIIcon from "@lobehub/icons/es/OpenAI"; -import OpenRouterIcon from "@lobehub/icons/es/OpenRouter"; -import TogetherIcon from "@lobehub/icons/es/Together"; +import anthropicLogo from "@lobehub/icons-static-svg/icons/anthropic.svg"; +import deepSeekLogo from "@lobehub/icons-static-svg/icons/deepseek.svg"; +import groqLogo from "@lobehub/icons-static-svg/icons/groq.svg"; +import mistralLogo from "@lobehub/icons-static-svg/icons/mistral.svg"; +import openAiLogo from "@lobehub/icons-static-svg/icons/openai.svg"; +import openRouterLogo from "@lobehub/icons-static-svg/icons/openrouter.svg"; +import togetherLogo from "@lobehub/icons-static-svg/icons/together.svg"; import { Accordion, Alert, @@ -39,7 +39,6 @@ import { } from "@heroui/react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import Fuse from "fuse.js"; -import type React from "react"; import { type Key, useMemo, useState } from "react"; import { Controller, useForm, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; @@ -175,10 +174,8 @@ interface ProviderModelFormValue { name: string; } -let nextProviderModelId = 0; - function createProviderModelFormValue(name = ""): ProviderModelFormValue { - const id = `provider-model-${nextProviderModelId++}`; + const id = `provider-model-${crypto.randomUUID()}`; return { id, name }; } @@ -187,34 +184,36 @@ function toProviderModelFormValues(models: string[]) { } function ProviderIcon({ format }: { format: InferenceProviderFormatDto }) { - const Icon = format === "anthropic" ? AnthropicIcon : OpenAIIcon; + const src = format === "anthropic" ? anthropicLogo : openAiLogo; return (
-
); } -const PRESET_LOGO_MAP: Record< - string, - React.ComponentType<{ size?: number; "aria-hidden"?: boolean }> -> = { - OpenAI: OpenAIIcon, - Anthropic: AnthropicIcon, - OpenRouter: OpenRouterIcon, - Groq: GroqIcon, - Mistral: MistralIcon, - Together: TogetherIcon, - DeepSeek: DeepSeekIcon, +const PRESET_LOGO_MAP: Record = { + OpenAI: openAiLogo, + Anthropic: anthropicLogo, + OpenRouter: openRouterLogo, + Groq: groqLogo, + Mistral: mistralLogo, + Together: togetherLogo, + DeepSeek: deepSeekLogo, }; function PresetLogo({ logo, size = 16 }: { logo: string; size?: number }) { - const Icon = PRESET_LOGO_MAP[logo]; + const src = PRESET_LOGO_MAP[logo]; return (
- {Icon ? ( - + {src ? ( + ) : ( )} diff --git a/crates/desktop/src/pages/inference-providers/claude-panel.tsx b/crates/desktop/src/pages/inference-providers/claude-panel.tsx index 50128d20..165fcb48 100644 --- a/crates/desktop/src/pages/inference-providers/claude-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/claude-panel.tsx @@ -20,7 +20,7 @@ import { toast, } from "@heroui/react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import type { FormEvent } from "react"; import { useTranslation } from "react-i18next"; import type { @@ -37,7 +37,9 @@ import { deleteClaudeProviderMutationOptions, inferenceProviderListQueryOptions, syncClaudeProviderMutationOptions, + updateClaudeProviderMutationOptions, } from "../../requests/inference-providers"; +import { selectValidProviderId } from "./provider-selection"; function ClaudeCreateProviderDialog({ isOpen, @@ -63,11 +65,11 @@ function ClaudeCreateProviderDialog({ [inventoryProviders], ); const defaultProviderId = anthropicProviders[0]?.id ?? ""; - - useEffect(() => { - if (!isOpen) return; - setSelectedProviderId((current) => current || defaultProviderId); - }, [defaultProviderId, isOpen]); + const effectiveSelectedProviderId = selectValidProviderId( + selectedProviderId, + anthropicProviders, + defaultProviderId, + ); const createMutation = useMutation({ ...createClaudeProviderMutationOptions({ @@ -78,18 +80,25 @@ function ClaudeCreateProviderDialog({ onClose(); }, }), + onError: (error) => { + console.error("Failed to create Claude provider:", error); + toast.danger( + error instanceof Error + ? error.message + : t("claudeProviderUpdateError"), + ); + }, }); - const activeError = createMutation.error; const isPending = createMutation.isPending; const hasAnthropicProviders = anthropicProviders.length > 0; const handleSubmit = (event: FormEvent) => { event.preventDefault(); - if (!selectedProviderId) return; + if (!effectiveSelectedProviderId) return; createMutation.mutate({ - inference_provider_id: selectedProviderId, + inference_provider_id: effectiveSelectedProviderId, }); }; @@ -110,19 +119,6 @@ function ClaudeCreateProviderDialog({
- {activeError && ( - - - - - {activeError instanceof Error - ? activeError.message - : String(activeError)} - - - - )} - {isInventoryLoading && (
@@ -144,7 +140,7 @@ function ClaudeCreateProviderDialog({ { if (!key) return; @@ -202,7 +203,7 @@ function CodexCreateProviderDialog({ isDisabled={ isInventoryLoading || !hasResponseProviders || - !selectedProviderId + !effectiveSelectedProviderId } > {t("add")} diff --git a/crates/desktop/src/pages/inference-providers/opencode-panel.tsx b/crates/desktop/src/pages/inference-providers/opencode-panel.tsx index a64a6475..8fe434e8 100644 --- a/crates/desktop/src/pages/inference-providers/opencode-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/opencode-panel.tsx @@ -41,6 +41,7 @@ import { syncOpenCodeProviderMutationOptions, updateOpenCodeProviderMutationOptions, } from "../../requests/inference-providers"; +import { selectValidProviderId } from "./provider-selection"; type ProviderDialogMode = | { type: "create" } @@ -63,10 +64,11 @@ function OpenCodeCreateProviderDialog({ const [selectedProviderId, setSelectedProviderId] = useState(""); const defaultProviderId = inventoryProviders[0]?.id ?? ""; - useEffect(() => { - if (!isOpen) return; - setSelectedProviderId((current) => current || defaultProviderId); - }, [defaultProviderId, isOpen]); + const effectiveSelectedProviderId = selectValidProviderId( + selectedProviderId, + inventoryProviders, + defaultProviderId, + ); const createMutation = useMutation({ ...createOpenCodeProviderMutationOptions({ @@ -85,10 +87,10 @@ function OpenCodeCreateProviderDialog({ const handleSubmit = (event: FormEvent) => { event.preventDefault(); - if (!selectedProviderId) return; + if (!effectiveSelectedProviderId) return; createMutation.mutate({ - inference_provider_id: selectedProviderId, + inference_provider_id: effectiveSelectedProviderId, }); }; @@ -145,7 +147,7 @@ function OpenCodeCreateProviderDialog({ { - if (key === null) return; - handleAgentClick(key as CodingAgentId); - }} - > - - - - - - - {filteredCodingAgents.map((agent) => ( - -
- - - {agent.label} - -
- -
- ))} -
-
- - )} -
+ {filteredCodingAgents.length === 0 ? ( +
+

+ {searchQuery.trim() + ? t("noCodingAgentsMatch") + : t("noAgentsAvailable")} +

+
+ ) : ( + { + if (keys === "all") return; + const agentId = [...keys][0] as + | CodingAgentId + | undefined; + if (!agentId) return; + handleAgentClick(agentId); + }} + className="p-2" + > + {filteredCodingAgents.map((agent) => ( + +
+ +
+ +
+
+
+ ))} +
+ )} Date: Mon, 4 May 2026 00:50:49 +0800 Subject: [PATCH 43/62] fix(inference): preserve applied migration checksums --- .../0005_create_agent_provider_bindings.sql | 13 +- .../0006_drop_binding_is_active.sql | 13 +- .../0007_scope_agent_provider_binding_ids.sql | 40 +++++ crates/inference/src/store.rs | 149 ++++++++++++++++++ 4 files changed, 191 insertions(+), 24 deletions(-) create mode 100644 crates/inference/migrations/0007_scope_agent_provider_binding_ids.sql diff --git a/crates/inference/migrations/0005_create_agent_provider_bindings.sql b/crates/inference/migrations/0005_create_agent_provider_bindings.sql index d49bd1d6..0ef2bd39 100644 --- a/crates/inference/migrations/0005_create_agent_provider_bindings.sql +++ b/crates/inference/migrations/0005_create_agent_provider_bindings.sql @@ -6,14 +6,13 @@ -- already have native multi-provider support (e.g. OpenCode) should NOT -- use this table. CREATE TABLE IF NOT EXISTS agent_provider_bindings ( - id TEXT NOT NULL, + id TEXT PRIMARY KEY NOT NULL, agent_id TEXT NOT NULL, inference_provider_id TEXT NOT NULL, is_active INTEGER NOT NULL DEFAULT 0, model TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE (agent_id, id), FOREIGN KEY (inference_provider_id) REFERENCES inference_providers(id) ON DELETE CASCADE @@ -24,13 +23,3 @@ ON agent_provider_bindings(agent_id); CREATE INDEX IF NOT EXISTS idx_agent_provider_bindings_active ON agent_provider_bindings(agent_id, is_active); - -CREATE TRIGGER IF NOT EXISTS trg_agent_provider_bindings_updated_at -AFTER UPDATE ON agent_provider_bindings -FOR EACH ROW -WHEN NEW.updated_at = OLD.updated_at -BEGIN - UPDATE agent_provider_bindings - SET updated_at = datetime('now') - WHERE agent_id = NEW.agent_id AND id = NEW.id; -END; diff --git a/crates/inference/migrations/0006_drop_binding_is_active.sql b/crates/inference/migrations/0006_drop_binding_is_active.sql index ca38ac93..df6150f7 100644 --- a/crates/inference/migrations/0006_drop_binding_is_active.sql +++ b/crates/inference/migrations/0006_drop_binding_is_active.sql @@ -6,13 +6,12 @@ -- SQLite does not support DROP COLUMN IF EXISTS, so we recreate the -- table while ignoring the is_active column if it exists. CREATE TABLE IF NOT EXISTS agent_provider_bindings_new ( - id TEXT NOT NULL, + id TEXT PRIMARY KEY NOT NULL, agent_id TEXT NOT NULL, inference_provider_id TEXT NOT NULL, model TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE (agent_id, id), FOREIGN KEY (inference_provider_id) REFERENCES inference_providers(id) ON DELETE CASCADE @@ -31,13 +30,3 @@ ALTER TABLE agent_provider_bindings_new RENAME TO agent_provider_bindings; CREATE INDEX IF NOT EXISTS idx_agent_provider_bindings_agent ON agent_provider_bindings(agent_id); - -CREATE TRIGGER IF NOT EXISTS trg_agent_provider_bindings_updated_at -AFTER UPDATE ON agent_provider_bindings -FOR EACH ROW -WHEN NEW.updated_at = OLD.updated_at -BEGIN - UPDATE agent_provider_bindings - SET updated_at = datetime('now') - WHERE agent_id = NEW.agent_id AND id = NEW.id; -END; diff --git a/crates/inference/migrations/0007_scope_agent_provider_binding_ids.sql b/crates/inference/migrations/0007_scope_agent_provider_binding_ids.sql new file mode 100644 index 00000000..4290bd35 --- /dev/null +++ b/crates/inference/migrations/0007_scope_agent_provider_binding_ids.sql @@ -0,0 +1,40 @@ +-- Scope agent-provider binding ids by agent and maintain updated_at. +-- +-- Binding ids can be stable public ids from agent config files. Different +-- agents may legitimately use the same id, so uniqueness must be per agent +-- rather than global. +CREATE TABLE IF NOT EXISTS agent_provider_bindings_new ( + id TEXT NOT NULL, + agent_id TEXT NOT NULL, + inference_provider_id TEXT NOT NULL, + model TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE (agent_id, id), + FOREIGN KEY (inference_provider_id) + REFERENCES inference_providers(id) + ON DELETE CASCADE +); + +INSERT INTO agent_provider_bindings_new + (id, agent_id, inference_provider_id, model, created_at, updated_at) +SELECT + id, agent_id, inference_provider_id, model, created_at, updated_at +FROM agent_provider_bindings; + +DROP TABLE agent_provider_bindings; + +ALTER TABLE agent_provider_bindings_new RENAME TO agent_provider_bindings; + +CREATE INDEX IF NOT EXISTS idx_agent_provider_bindings_agent +ON agent_provider_bindings(agent_id); + +CREATE TRIGGER IF NOT EXISTS trg_agent_provider_bindings_updated_at +AFTER UPDATE ON agent_provider_bindings +FOR EACH ROW +WHEN NEW.updated_at = OLD.updated_at +BEGIN + UPDATE agent_provider_bindings + SET updated_at = datetime('now') + WHERE agent_id = NEW.agent_id AND id = NEW.id; +END; diff --git a/crates/inference/src/store.rs b/crates/inference/src/store.rs index 70f3ef17..675ff848 100644 --- a/crates/inference/src/store.rs +++ b/crates/inference/src/store.rs @@ -901,6 +901,155 @@ mod tests { .unwrap() } + #[test] + fn test_migrates_existing_v6_binding_database() { + let (temp, store) = store(); + let db_path = temp.path().join(INFERENCE_PROVIDERS_FILE); + store.block_on(async { + let mut conn = SqliteConnectOptions::new() + .filename(&db_path) + .create_if_missing(true) + .connect() + .await + .unwrap(); + sqlx::query( + "CREATE TABLE _sqlx_migrations ( + version BIGINT PRIMARY KEY, + description TEXT NOT NULL, + installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + success BOOLEAN NOT NULL, + checksum BLOB NOT NULL, + execution_time BIGINT NOT NULL + )", + ) + .execute(&mut conn) + .await + .unwrap(); + sqlx::query( + "CREATE TABLE inference_providers ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL UNIQUE, + format TEXT NOT NULL, + api_base_url TEXT NOT NULL, + masked_api_key TEXT NOT NULL DEFAULT '', + display_name TEXT NOT NULL DEFAULT '' + )", + ) + .execute(&mut conn) + .await + .unwrap(); + sqlx::query( + "CREATE TABLE inference_models ( + id TEXT PRIMARY KEY NOT NULL, + provider_id TEXT NOT NULL, + name TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (provider_id) + REFERENCES inference_providers(id) + ON DELETE CASCADE, + UNIQUE (provider_id, name) + )", + ) + .execute(&mut conn) + .await + .unwrap(); + sqlx::query( + "CREATE TABLE agent_provider_bindings ( + id TEXT PRIMARY KEY NOT NULL, + agent_id TEXT NOT NULL, + inference_provider_id TEXT NOT NULL, + model TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (inference_provider_id) + REFERENCES inference_providers(id) + ON DELETE CASCADE + )", + ) + .execute(&mut conn) + .await + .unwrap(); + + let migrations = [ + ( + 1, + "create inference providers", + "bf935d5229df4e204f7e0cc2f14721dbfeb45c9a15c229dca50127407ec9fc2311906fa98f567db786f9bf3a4dbc7412", + ), + ( + 2, + "create inference models", + "95502c735b08fa0f0074c6885090be68f2b033da1c918e0a825279169faace7fe9fa4fd64b08bf2417f892d65597d0fd", + ), + ( + 3, + "add masked api key", + "ff15b82d3ce15332f0ee9c0264db8340326bfa025fb554c32275c33b1be11d29cea5ad4386f4c2c2350e42c61a145b1e", + ), + ( + 4, + "add display name", + "5bdac59333690e70da935d043ababf088fe53f7c836a250855472288b38bda5952c3117936bf1b054f662adb3cd7ce48", + ), + ( + 5, + "create agent provider bindings", + "14ca7c3e31001e23f4646d99d9dbd27badfcc4bf595eec9ccd4bcbe7bd69e17cae9d5c1eba1ea515764b395c41abf27a", + ), + ( + 6, + "drop binding is active", + "a442005806c4fa8c07d3beab28091ede276eea39ea6529df31c1bfa784ea70f14320223fd513d5a84f6121371800417c", + ), + ]; + for (version, description, checksum) in migrations { + let query = format!( + "INSERT INTO _sqlx_migrations + (version, description, success, checksum, execution_time) + VALUES ({version}, '{description}', 1, x'{checksum}', 0)" + ); + sqlx::query(&query).execute(&mut conn).await.unwrap(); + } + }); + + assert!(store.list().unwrap().is_empty()); + + store.block_on(async { + let mut conn = SqliteConnectOptions::new() + .filename(&db_path) + .connect() + .await + .unwrap(); + let version: i64 = + sqlx::query_scalar("SELECT MAX(version) FROM _sqlx_migrations") + .fetch_one(&mut conn) + .await + .unwrap(); + assert_eq!(version, 7); + + let trigger_count: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM sqlite_master + WHERE type = 'trigger' + AND name = 'trg_agent_provider_bindings_updated_at'", + ) + .fetch_one(&mut conn) + .await + .unwrap(); + assert_eq!(trigger_count, 1); + + let unique_count: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM pragma_index_list( + 'agent_provider_bindings' + ) + WHERE [unique] = 1", + ) + .fetch_one(&mut conn) + .await + .unwrap(); + assert_eq!(unique_count, 1); + }); + } + #[test] fn test_list_missing_file_is_empty() { let (_temp, store) = store(); From a05efc96cb7f1abef0a9d3c57894726f942bc04a Mon Sep 17 00:00:00 2001 From: Flacier Date: Mon, 4 May 2026 21:07:01 +0800 Subject: [PATCH 44/62] fix(desktop): render preset logos as inline SVG for dark mode support - Import SVGs with ?raw to get inline markup instead of URL strings - Use dangerouslySetInnerHTML so currentColor inherits text-foreground - Fix icon alignment from items-start to items-center --- .../desktop/src/pages/inference-providers.tsx | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index 9caa6639..72d51a7e 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -10,13 +10,13 @@ import { ServerIcon, TrashIcon, } from "@heroicons/react/24/solid"; -import anthropicLogo from "@lobehub/icons-static-svg/icons/anthropic.svg"; -import deepSeekLogo from "@lobehub/icons-static-svg/icons/deepseek.svg"; -import groqLogo from "@lobehub/icons-static-svg/icons/groq.svg"; -import mistralLogo from "@lobehub/icons-static-svg/icons/mistral.svg"; -import openAiLogo from "@lobehub/icons-static-svg/icons/openai.svg"; -import openRouterLogo from "@lobehub/icons-static-svg/icons/openrouter.svg"; -import togetherLogo from "@lobehub/icons-static-svg/icons/together.svg"; +import anthropicLogo from "@lobehub/icons-static-svg/icons/anthropic.svg?raw"; +import deepSeekLogo from "@lobehub/icons-static-svg/icons/deepseek.svg?raw"; +import groqLogo from "@lobehub/icons-static-svg/icons/groq.svg?raw"; +import mistralLogo from "@lobehub/icons-static-svg/icons/mistral.svg?raw"; +import openAiLogo from "@lobehub/icons-static-svg/icons/openai.svg?raw"; +import openRouterLogo from "@lobehub/icons-static-svg/icons/openrouter.svg?raw"; +import togetherLogo from "@lobehub/icons-static-svg/icons/together.svg?raw"; import { Accordion, Alert, @@ -206,21 +206,18 @@ const PRESET_LOGO_MAP: Record = { DeepSeek: deepSeekLogo, }; -function PresetLogo({ logo, size = 16 }: { logo: string; size?: number }) { - const src = PRESET_LOGO_MAP[logo]; +function PresetLogo({ logo }: { logo: string }) { + const svg = PRESET_LOGO_MAP[logo]; + if (!svg) { + return ( + + ); + } return ( -
- {src ? ( - - ) : ( - - )} -
+ ); } @@ -958,7 +955,7 @@ function ProviderForm({ id={preset.id} textValue={preset.name} > -
+
From 34cee89f2fef5e36195b1720f6eb057cddd8971f Mon Sep 17 00:00:00 2001 From: Flacier Date: Thu, 7 May 2026 23:28:54 +0800 Subject: [PATCH 45/62] feat(desktop): add show/hide toggle for API key input in provider form - Use HeroUI InputGroup with eye icon suffix - Standard password visibility toggle pattern --- .../desktop/src/pages/inference-providers.tsx | 68 +++++++++++++------ 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index 72d51a7e..4e5afbce 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -28,6 +28,7 @@ import { Fieldset, Form, Input, + InputGroup, Label, ListBox, SearchField, @@ -797,6 +798,7 @@ function ProviderForm({ if (preset) handleApplyPreset(preset); }; + const [showApiKey, setShowApiKey] = useState(false); const watchedApiBaseUrl = useWatch({ control, name: "apiBaseUrl" }); const watchedApiKey = useWatch({ control, name: "apiKey" }); const canFetchModels = Boolean( @@ -1190,26 +1192,54 @@ function ProviderForm({ )}
- - field.onChange( - event.target.value, - ) - } - onBlur={field.onBlur} - placeholder={ - mode === "create" - ? t( - "providerApiKeyPlaceholder", - ) - : t( - "providerApiKeyEditPlaceholder", + + + field.onChange( + event.target.value, + ) + } + onBlur={field.onBlur} + placeholder={ + mode === "create" + ? t( + "providerApiKeyPlaceholder", + ) + : t( + "providerApiKeyEditPlaceholder", + ) + } + /> + + + + {fieldState.error && ( {fieldState.error.message} From 3929cf5c607d381dae262b90772fa85a3a2b4bec Mon Sep 17 00:00:00 2001 From: Flacier Date: Thu, 7 May 2026 23:34:18 +0800 Subject: [PATCH 46/62] fix(desktop): render ProviderIcon as inline SVG matching PresetLogo pattern --- crates/desktop/src/pages/inference-providers.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index 4e5afbce..a202d72e 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -188,12 +188,13 @@ function toProviderModelFormValues(models: string[]) { } function ProviderIcon({ format }: { format: InferenceProviderFormatDto }) { - const src = format === "anthropic" ? anthropicLogo : openAiLogo; + const svg = format === "anthropic" ? anthropicLogo : openAiLogo; return ( -
- -
+ ); } From 502b95ddbd9b3c3202d53c22e2c5838643be78ac Mon Sep 17 00:00:00 2001 From: Flacier Date: Fri, 8 May 2026 15:46:13 +0800 Subject: [PATCH 47/62] refactor(desktop): use Tauri clipboard plugin in inference providers page - Replace navigator.clipboard.writeText with static import from @tauri-apps/plugin-clipboard-manager - Aligns with main branch clipboard migration (4246c00) --- crates/desktop/src/pages/inference-providers.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/desktop/src/pages/inference-providers.tsx b/crates/desktop/src/pages/inference-providers.tsx index a202d72e..029c38c4 100644 --- a/crates/desktop/src/pages/inference-providers.tsx +++ b/crates/desktop/src/pages/inference-providers.tsx @@ -62,6 +62,7 @@ import { inferenceProviderPresetsQueryOptions, updateInferenceProviderMutationOptions, } from "../requests/inference-providers"; +import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { openUrl } from "@tauri-apps/plugin-opener"; import type { InferenceProviderPresetResponse } from "../generated/dto"; import { useAgentAvailability } from "../hooks/use-agent-availability"; @@ -1364,7 +1365,7 @@ function ProviderDetail({ const password = revealedKey ? { api_key: revealedKey } : await api.inferenceProviders.getPassword(provider.name); - await navigator.clipboard.writeText(password.api_key); + await writeText(password.api_key); toast.success(t("providerApiKeyCopied")); } catch (error) { console.error("Failed to copy inference provider key:", error); @@ -1380,7 +1381,7 @@ function ProviderDetail({ const handleCopyModel = async (modelName: string) => { try { - await navigator.clipboard.writeText(modelName); + await writeText(modelName); toast.success(t("providerModelNameCopied")); } catch (error) { console.error("Failed to copy inference model name:", error); From 16e14c00c5727d44ba2d5e69432cbd32155f1f47 Mon Sep 17 00:00:00 2001 From: danielchim Date: Sat, 9 May 2026 22:17:10 +0800 Subject: [PATCH 48/62] chore(desktop): simplify nullish api_key fallback in opencode panel Replace the ternary `trimmedApiKey ? trimmedApiKey : null` with the shorter `trimmedApiKey || null` form. Behavior is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/desktop/src/pages/inference-providers/opencode-panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/desktop/src/pages/inference-providers/opencode-panel.tsx b/crates/desktop/src/pages/inference-providers/opencode-panel.tsx index 8fe434e8..181038b6 100644 --- a/crates/desktop/src/pages/inference-providers/opencode-panel.tsx +++ b/crates/desktop/src/pages/inference-providers/opencode-panel.tsx @@ -261,7 +261,7 @@ function OpenCodeEditProviderDialog({ id: provider.id, body: { name: trimmedName === provider.name ? null : trimmedName, - api_key: trimmedApiKey ? trimmedApiKey : null, + api_key: trimmedApiKey || null, }, }); }; From 1371009b71cd774dedce814ec4623cf43f967de6 Mon Sep 17 00:00:00 2001 From: danielchim Date: Sat, 9 May 2026 22:17:26 +0800 Subject: [PATCH 49/62] feat(desktop): add home page with agent overview cards Introduces the new `/` landing page that frames the product as "your agent hub": a grid of installed agents (sorted ready first, then disabled, then alphabetical), each card showing skill / MCP counts and an Open button. Includes an "Add agent" placeholder card and a quick-actions row to the market and agent settings. The page is unreachable until the route table and sidebar wire it up in a later commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/desktop/src/pages/home.tsx | 223 ++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 crates/desktop/src/pages/home.tsx diff --git a/crates/desktop/src/pages/home.tsx b/crates/desktop/src/pages/home.tsx new file mode 100644 index 00000000..7eed4670 --- /dev/null +++ b/crates/desktop/src/pages/home.tsx @@ -0,0 +1,223 @@ +import { ArrowRightIcon, PlusIcon } from "@heroicons/react/24/solid"; +import { Button, Card } from "@heroui/react"; +import { useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useLocation } from "wouter"; +import type { AvailableAgent } from "../contexts/agent-availability"; +import { useAgentAvailability } from "../hooks/use-agent-availability"; +import { useApi } from "../hooks/use-api"; +import { AgentIcon } from "../lib/agent-icons"; +import { cn } from "../lib/utils"; +import { mcpListQueryOptions } from "../requests/mcps"; +import { skillListQueryOptions } from "../requests/skills"; + +type AgentStatus = "ready" | "missing" | "disabled"; + +function statusOf(agent: AvailableAgent): AgentStatus { + if (agent.isDisabled) return "disabled"; + if (!agent.availability.is_available) return "missing"; + return "ready"; +} + +function StatusBadge({ status }: { status: AgentStatus }) { + const { t } = useTranslation(); + const tone = + status === "ready" + ? "bg-success/15 text-success" + : status === "missing" + ? "bg-warning/15 text-warning" + : "bg-muted/15 text-muted"; + const labelKey = + status === "ready" + ? "agentStatusReady" + : status === "missing" + ? "agentStatusMissing" + : "agentStatusDisabled"; + return ( + + + {t(labelKey)} + + ); +} + +function AgentOverviewCard({ agent }: { agent: AvailableAgent }) { + const { t } = useTranslation(); + const api = useApi(); + const [, setLocation] = useLocation(); + const status = statusOf(agent); + + const { data: skills = [] } = useQuery({ + ...skillListQueryOptions({ api, scope: "global" }), + }); + const { data: mcps = [] } = useQuery({ + ...mcpListQueryOptions({ api, scope: "global" }), + }); + + const skillCount = useMemo( + () => skills.filter((s) => !s.agent || s.agent === agent.id).length, + [skills, agent.id], + ); + const mcpCount = useMemo( + () => mcps.filter((m) => !m.agent || m.agent === agent.id).length, + [mcps, agent.id], + ); + + return ( + + + +
+ + {agent.display_name} + +
+ +
+
+
+ + {status === "ready" ? ( +
+
+
+ {t("skills")} +
+
+ {skillCount} +
+
+
+
+ {t("mcp")} +
+
+ {mcpCount} +
+
+
+ ) : ( +

+ {status === "missing" + ? t("agentMissingHint") + : t("agentDisabledHint")} +

+ )} +
+ +
+
+
+ ); +} + +export default function HomePage() { + const { t } = useTranslation(); + const [, setLocation] = useLocation(); + const { availableAgents } = useAgentAvailability(); + + const installedAgents = useMemo( + () => + availableAgents.filter((agent) => agent.availability.is_available), + [availableAgents], + ); + + const sortedAgents = useMemo(() => { + const order = { ready: 0, missing: 1, disabled: 2 } as const; + return [...installedAgents].sort((a, b) => { + const sa = order[statusOf(a)]; + const sb = order[statusOf(b)]; + if (sa !== sb) return sa - sb; + return a.display_name.localeCompare(b.display_name); + }); + }, [installedAgents]); + + const readyCount = useMemo( + () => installedAgents.filter((a) => statusOf(a) === "ready").length, + [installedAgents], + ); + + return ( +
+
+
+

+ {t("homeTitle")} +

+

+ {t("homeSubtitle", { + ready: readyCount, + total: installedAgents.length, + })} +

+
+ +
+ {sortedAgents.map((agent) => ( + + ))} + + + +

+ {t("addAgent")} +

+

+ {t("addAgentHint")} +

+ +
+
+
+ +
+

+ {t("quickActions")} +

+
+ + +
+
+
+
+ ); +} From cc38a45018e990c64af5c267b3a2f5679bcda063 Mon Sep 17 00:00:00 2001 From: danielchim Date: Sat, 9 May 2026 22:17:48 +0800 Subject: [PATCH 50/62] feat(desktop): add per-agent detail page with skills/mcp/model/sub-agents tabs Introduces `/agents/:agentId` as the agent-scoped workspace. The header shows the agent's icon, name, scope hint, and a primary "Open config folder" button that uses Tauri's revealItemInDir to surface the agent's actual config file in Finder/Explorer. Tabs reuse the existing global pages as embedded views so list behaviour stays consistent. The Model tab renders the inference provider page for Claude/Codex/OpenCode and shows a "managed by the agent" placeholder for the rest. Adds `lib/agent-config-paths.ts` which resolves a best-known filesystem path per agent: hardcoded `~`-relative paths for the 13 agents whose locations are well-known (`~/.claude.json`, `~/.codex/config.toml`, `~/.config/opencode/opencode.json`, etc.) falling back to `skills_paths.global_write` for the rest. Returns null for agents we don't recognise so the button is hidden. The route is unreachable until the App route table wires it up. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/desktop/src/lib/agent-config-paths.ts | 43 ++++ crates/desktop/src/pages/agent/index.tsx | 216 +++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 crates/desktop/src/lib/agent-config-paths.ts create mode 100644 crates/desktop/src/pages/agent/index.tsx diff --git a/crates/desktop/src/lib/agent-config-paths.ts b/crates/desktop/src/lib/agent-config-paths.ts new file mode 100644 index 00000000..765a7a09 --- /dev/null +++ b/crates/desktop/src/lib/agent-config-paths.ts @@ -0,0 +1,43 @@ +import { homeDir, join } from "@tauri-apps/api/path"; +import type { AgentInfo } from "../generated/dto"; + +const KNOWN_AGENT_RELATIVE_PATHS: Record = { + claude: [".claude.json"], + codex: [".codex", "config.toml"], + opencode: [".config", "opencode", "opencode.json"], + cursor: [".cursor"], + windsurf: [".codeium", "windsurf"], + zed: [".config", "zed"], + warp: [".warp"], + copilot: [".claude.json"], + cline: [".config", "cline"], + gemini: [".config", "gemini"], + roocode: [".config", "roocode"], + mistral: [".config", "mistral"], + amp: [".config", "amp"], +}; + +/** + * Resolve a filesystem path that we can hand to revealItemInDir for an agent. + * + * Strategy: + * 1. Look up a hardcoded relative-to-home path for known agents (most accurate + * — points at the actual config file or directory). + * 2. Fall back to the agent's global skills write directory if it has one. + * 3. Return null if neither is known (caller should hide the button). + */ +export async function resolveAgentConfigPath( + agent: AgentInfo, +): Promise { + const known = KNOWN_AGENT_RELATIVE_PATHS[agent.id]; + if (known) { + const home = await homeDir(); + return join(home, ...known); + } + + if (agent.skills_paths.global_write) { + return agent.skills_paths.global_write; + } + + return null; +} diff --git a/crates/desktop/src/pages/agent/index.tsx b/crates/desktop/src/pages/agent/index.tsx new file mode 100644 index 00000000..4f150f17 --- /dev/null +++ b/crates/desktop/src/pages/agent/index.tsx @@ -0,0 +1,216 @@ +import { FolderOpenIcon } from "@heroicons/react/24/solid"; +import { Button, Surface, Tabs, toast } from "@heroui/react"; +import { useQuery } from "@tanstack/react-query"; +import { revealItemInDir } from "@tauri-apps/plugin-opener"; +import { Suspense } from "react"; +import { useTranslation } from "react-i18next"; +import { Route, Switch, useLocation, useParams } from "wouter"; +import { Redirect } from "../../components/redirect"; +import { ErrorBoundary } from "../../components/ui/error-boundary"; +import type { AvailableAgent } from "../../contexts/agent-availability"; +import { useAgentAvailability } from "../../hooks/use-agent-availability"; +import { resolveAgentConfigPath } from "../../lib/agent-config-paths"; +import { supportsMcp, supportsSkill } from "../../lib/agent-capabilities"; +import { AgentIcon } from "../../lib/agent-icons"; +import InferenceProvidersPage from "../inference-providers"; +import MCPServersPage from "../settings/mcp-servers"; +import SkillsPage from "../settings/skills"; +import SubAgentsPage from "../settings/sub-agents"; + +const CODING_AGENT_IDS = new Set(["claude", "codex", "opencode"]); + +function AgentHeader({ agent }: { agent: AvailableAgent }) { + const { t } = useTranslation(); + + const { data: configPath } = useQuery({ + queryKey: ["agent-config-path", agent.id], + queryFn: () => resolveAgentConfigPath(agent), + staleTime: Infinity, + }); + + const handleOpenConfigFolder = async () => { + if (!configPath) return; + try { + await revealItemInDir(configPath); + } catch (error) { + console.error( + `Failed to reveal config folder for ${agent.id}:`, + error, + ); + toast.danger( + t("openAgentConfigFolderFailed", { + name: agent.display_name, + }), + ); + } + }; + + return ( +
+ +
+

+ {agent.display_name} +

+

+ {t("agentScopeHint", { name: agent.display_name })} +

+
+ {configPath && ( + + )} +
+ ); +} + +function ModelTab({ agent }: { agent: AvailableAgent }) { + const { t } = useTranslation(); + if (!CODING_AGENT_IDS.has(agent.id)) { + return ( +
+
+

+ {t("modelNotConfigurableTitle")} +

+

+ {t("modelNotConfigurableHint", { + name: agent.display_name, + })} +

+
+
+ ); + } + return ; +} + +function TabFallback() { + return
; +} + +export default function AgentDetailPage() { + const { t } = useTranslation(); + const { agentId } = useParams(); + const [pathname, setLocation] = useLocation(); + const { availableAgents } = useAgentAvailability(); + + const agent = availableAgents.find((a) => a.id === agentId); + + if (!agent) { + return ; + } + + const tabs: Array<{ id: string; labelKey: string; href: string }> = []; + if (supportsSkill(agent)) { + tabs.push({ + id: "skills", + labelKey: "skills", + href: `/agents/${agent.id}/skills`, + }); + } + if (supportsMcp(agent)) { + tabs.push({ + id: "mcp", + labelKey: "mcp", + href: `/agents/${agent.id}/mcp`, + }); + } + tabs.push({ + id: "model", + labelKey: "model", + href: `/agents/${agent.id}/model`, + }); + tabs.push({ + id: "sub-agents", + labelKey: "subAgents", + href: `/agents/${agent.id}/sub-agents`, + }); + + const activeTabId = + tabs.find((tab) => pathname.startsWith(tab.href))?.id ?? + tabs[0]?.id ?? + "skills"; + + const handleTabChange = (id: string) => { + const tab = tabs.find((t) => t.id === id); + if (tab) setLocation(tab.href); + }; + + const defaultTabHref = tabs[0]?.href ?? `/agents/${agent.id}/skills`; + + return ( +
+ + + handleTabChange(String(key))} + > + + + {tabs.map((tab) => ( + + {t(tab.labelKey)} + + + ))} + + + + + +
+ + + + + + + }> + + + + + + + }> + + + + + + + }> + + + + + + + }> + + + + + +
+
+ ); +} From 39cefc2bdd48f67ab0eac08af950356ac2c12410 Mon Sep 17 00:00:00 2001 From: danielchim Date: Sat, 9 May 2026 22:18:07 +0800 Subject: [PATCH 51/62] feat(desktop): add global search with grouped popover and full results page Adds a single search box (intended to live in the sidebar) that fuzzy-matches across the user's installed agents, skills, MCP servers, sub-agents, and the skills.sh library catalogue. Results are grouped per category and capped per group in the popover. The popover supports keyboard navigation (arrow keys + enter on a highlighted row jumps to it). Pressing enter without a highlighted row, or clicking the always-visible "View all results" footer, navigates to the new `/search?q=...` page. The full-page view shows the same groups without per-group caps; sections with more than 8 rows collapse to a "Show N more" toggle so a 100+ skill list does not dominate the screen. `useGlobalSearch` extracts the matching logic so both the popover and the full page share one source of truth. Library results are debounced (250ms) and capped at 5 in the popover, 20 on the page. The search box is unreachable until a later commit mounts it in the sidebar. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../desktop/src/components/global-search.tsx | 236 ++++++++++++++++ crates/desktop/src/hooks/use-global-search.ts | 252 +++++++++++++++++ crates/desktop/src/pages/search.tsx | 257 ++++++++++++++++++ 3 files changed, 745 insertions(+) create mode 100644 crates/desktop/src/components/global-search.tsx create mode 100644 crates/desktop/src/hooks/use-global-search.ts create mode 100644 crates/desktop/src/pages/search.tsx diff --git a/crates/desktop/src/components/global-search.tsx b/crates/desktop/src/components/global-search.tsx new file mode 100644 index 00000000..4fff4df0 --- /dev/null +++ b/crates/desktop/src/components/global-search.tsx @@ -0,0 +1,236 @@ +import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline"; +import { ArrowRightIcon } from "@heroicons/react/24/solid"; +import { SearchField, Spinner } from "@heroui/react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useLocation } from "wouter"; +import type { AvailableAgent } from "../contexts/agent-availability"; +import { useAgentAvailability } from "../hooks/use-agent-availability"; +import { type SearchMatch, useGlobalSearch } from "../hooks/use-global-search"; +import { AgentIcon } from "../lib/agent-icons"; +import { cn } from "../lib/utils"; + +const PER_GROUP_LIMIT = 4; +const LIBRARY_LIMIT = 5; + +export function GlobalSearch() { + const { t } = useTranslation(); + const [, setLocation] = useLocation(); + const { availableAgents } = useAgentAvailability(); + const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + const [activeIndex, setActiveIndex] = useState(-1); + const containerRef = useRef(null); + + const { groups, isMarketFetching } = useGlobalSearch({ + query, + perGroupLimit: PER_GROUP_LIMIT, + libraryLimit: LIBRARY_LIMIT, + }); + + const trimmed = query.trim(); + + const flatMatches = useMemo(() => groups.flatMap((g) => g.items), [groups]); + + useEffect(() => { + setActiveIndex(-1); + }, [trimmed]); + + useEffect(() => { + if (!isOpen) return; + const handleClick = (event: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleClick); + return () => document.removeEventListener("mousedown", handleClick); + }, [isOpen]); + + const agentLookup = useMemo(() => { + const map = new Map(); + for (const agent of availableAgents) { + map.set(agent.id, agent); + } + return map; + }, [availableAgents]); + + const handleSelect = (match: SearchMatch) => { + setIsOpen(false); + setQuery(""); + setLocation(match.href); + }; + + const openSearchPage = () => { + if (!trimmed) return; + setIsOpen(false); + setQuery(""); + setLocation(`/search?q=${encodeURIComponent(trimmed)}`); + }; + + const showPopover = isOpen && trimmed.length > 0; + + return ( +
+ { + setQuery(value); + setIsOpen(true); + }} + aria-label={t("globalSearchLabel")} + variant="secondary" + className="w-full" + > + + + { + if (trimmed) setIsOpen(true); + }} + onKeyDown={(event) => { + if (event.key === "Escape") { + setIsOpen(false); + return; + } + if ( + event.key === "ArrowDown" && + flatMatches.length > 0 + ) { + event.preventDefault(); + setActiveIndex((i) => + Math.min(i + 1, flatMatches.length - 1), + ); + return; + } + if ( + event.key === "ArrowUp" && + flatMatches.length > 0 + ) { + event.preventDefault(); + setActiveIndex((i) => Math.max(i - 1, -1)); + return; + } + if (event.key === "Enter") { + event.preventDefault(); + const match = + activeIndex >= 0 + ? flatMatches[activeIndex] + : null; + if (match) { + handleSelect(match); + } else { + openSearchPage(); + } + } + }} + /> + + + + + {showPopover && ( +
+ {flatMatches.length === 0 && !isMarketFetching ? ( + <> +

+ {t("globalSearchNoResults")} +

+ + ) : ( + groups.map((group) => ( +
+
+ + {t(group.labelKey)} + +
+ {group.items.map((match) => { + const flatIdx = flatMatches.indexOf(match); + const isActive = flatIdx === activeIndex; + const owningAgent = match.agentId + ? agentLookup.get(match.agentId) + : null; + return ( + + ); + })} +
+ )) + )} + {isMarketFetching && ( +
+ + {t("globalSearchLibraryLoading")} +
+ )} + +
+ )} +
+ ); +} diff --git a/crates/desktop/src/hooks/use-global-search.ts b/crates/desktop/src/hooks/use-global-search.ts new file mode 100644 index 00000000..3acf4617 --- /dev/null +++ b/crates/desktop/src/hooks/use-global-search.ts @@ -0,0 +1,252 @@ +import { useQuery } from "@tanstack/react-query"; +import Fuse from "fuse.js"; +import { useEffect, useMemo, useState } from "react"; +import type { + MarketSkill, + McpResponse, + SkillResponse, + SubAgentResponse, +} from "../generated/dto"; +import { useAgentAvailability } from "./use-agent-availability"; +import { useApi } from "./use-api"; +import { mcpListQueryOptions } from "../requests/mcps"; +import { skillListQueryOptions } from "../requests/skills"; +import { subAgentListQueryOptions } from "../requests/sub-agents"; + +export type ResourceKind = "agent" | "skill" | "mcp" | "sub-agent" | "library"; + +export interface SearchMatch { + kind: ResourceKind; + id: string; + name: string; + subtitle: string | null; + agentId: string | null; + href: string; +} + +export interface SearchGroup { + kind: ResourceKind; + labelKey: string; + items: SearchMatch[]; +} + +const LIBRARY_DEBOUNCE_MS = 250; + +interface Options { + query: string; + perGroupLimit?: number; + libraryLimit?: number; +} + +function useDebouncedValue(value: T, delay: number): T { + const [debounced, setDebounced] = useState(value); + useEffect(() => { + const handle = window.setTimeout(() => setDebounced(value), delay); + return () => window.clearTimeout(handle); + }, [value, delay]); + return debounced; +} + +export function useGlobalSearch({ + query, + perGroupLimit, + libraryLimit = 5, +}: Options) { + const api = useApi(); + const { availableAgents } = useAgentAvailability(); + + const installedAgents = useMemo( + () => + availableAgents.filter((agent) => agent.availability.is_available), + [availableAgents], + ); + + const { data: skills = [] } = useQuery({ + ...skillListQueryOptions({ api, scope: "global" }), + }); + const { data: mcps = [] } = useQuery({ + ...mcpListQueryOptions({ api, scope: "global" }), + }); + const { data: subAgents = [] } = useQuery({ + ...subAgentListQueryOptions({ api, scope: "global" }), + }); + + const trimmed = query.trim(); + const debouncedTrimmed = useDebouncedValue(trimmed, LIBRARY_DEBOUNCE_MS); + + const { data: marketResults = [], isFetching: isMarketFetching } = useQuery< + MarketSkill[] + >({ + queryKey: ["global-search", "market", debouncedTrimmed, libraryLimit], + queryFn: () => api.market.search(debouncedTrimmed, libraryLimit), + enabled: debouncedTrimmed.length >= 2, + staleTime: 60_000, + }); + + const agentFuse = useMemo( + () => + new Fuse(installedAgents, { + keys: [ + { name: "display_name", weight: 2 }, + { name: "id", weight: 1 }, + ], + threshold: 0.4, + ignoreLocation: true, + }), + [installedAgents], + ); + const skillFuse = useMemo( + () => + new Fuse(skills, { + keys: [ + { name: "name", weight: 2 }, + { name: "description", weight: 1 }, + ], + threshold: 0.4, + ignoreLocation: true, + }), + [skills], + ); + const mcpFuse = useMemo( + () => + new Fuse(mcps, { + keys: ["name"], + threshold: 0.4, + ignoreLocation: true, + }), + [mcps], + ); + const subAgentFuse = useMemo( + () => + new Fuse(subAgents, { + keys: [ + { name: "name", weight: 2 }, + { name: "description", weight: 1 }, + ], + threshold: 0.4, + ignoreLocation: true, + }), + [subAgents], + ); + + const groups = useMemo(() => { + if (!trimmed) return []; + + const cap = (arr: { item: unknown }[]) => + perGroupLimit ? arr.slice(0, perGroupLimit) : arr; + + const agentMatches: SearchMatch[] = cap(agentFuse.search(trimmed)).map( + ({ item }) => { + const agent = item as { id: string; display_name: string }; + return { + kind: "agent", + id: agent.id, + name: agent.display_name, + subtitle: agent.id, + agentId: agent.id, + href: `/agents/${agent.id}`, + }; + }, + ); + + const skillMatches: SearchMatch[] = cap(skillFuse.search(trimmed)).map( + ({ item }) => { + const skill = item as SkillResponse; + return { + kind: "skill", + id: `${skill.agent ?? "all"}:${skill.name}`, + name: skill.name, + subtitle: skill.description, + agentId: skill.agent, + href: skill.agent + ? `/agents/${skill.agent}/skills?skill=${encodeURIComponent(skill.name)}` + : `/market/search?q=${encodeURIComponent(skill.name)}`, + }; + }, + ); + + const mcpMatches: SearchMatch[] = cap(mcpFuse.search(trimmed)).map( + ({ item }) => { + const mcp = item as McpResponse; + return { + kind: "mcp", + id: `${mcp.agent ?? "all"}:${mcp.name}`, + name: mcp.name, + subtitle: null, + agentId: mcp.agent, + href: mcp.agent + ? `/agents/${mcp.agent}/mcp?server=${encodeURIComponent(mcp.name)}` + : "/", + }; + }, + ); + + const subAgentMatches: SearchMatch[] = cap( + subAgentFuse.search(trimmed), + ).map(({ item }) => { + const subAgent = item as SubAgentResponse; + return { + kind: "sub-agent", + id: `${subAgent.agent ?? "all"}:${subAgent.name}`, + name: subAgent.name, + subtitle: subAgent.description, + agentId: subAgent.agent, + href: subAgent.agent + ? `/agents/${subAgent.agent}/sub-agents` + : "/", + }; + }); + + const libraryMatches: SearchMatch[] = marketResults.map((item) => ({ + kind: "library", + id: `library:${item.slug}`, + name: item.name, + subtitle: item.author ? `by ${item.author}` : null, + agentId: null, + href: `/market/search?q=${encodeURIComponent(trimmed)}`, + })); + + const out: SearchGroup[] = []; + if (agentMatches.length > 0) + out.push({ + kind: "agent", + labelKey: "yourAgents", + items: agentMatches, + }); + if (skillMatches.length > 0) + out.push({ + kind: "skill", + labelKey: "skills", + items: skillMatches, + }); + if (mcpMatches.length > 0) + out.push({ kind: "mcp", labelKey: "mcp", items: mcpMatches }); + if (subAgentMatches.length > 0) + out.push({ + kind: "sub-agent", + labelKey: "subAgents", + items: subAgentMatches, + }); + if (libraryMatches.length > 0) + out.push({ + kind: "library", + labelKey: "library", + items: libraryMatches, + }); + return out; + }, [ + trimmed, + agentFuse, + skillFuse, + mcpFuse, + subAgentFuse, + marketResults, + perGroupLimit, + ]); + + return { + query: trimmed, + groups, + isMarketFetching, + }; +} diff --git a/crates/desktop/src/pages/search.tsx b/crates/desktop/src/pages/search.tsx new file mode 100644 index 00000000..4a093772 --- /dev/null +++ b/crates/desktop/src/pages/search.tsx @@ -0,0 +1,257 @@ +import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline"; +import { + ChevronDownIcon, + MagnifyingGlassIcon, +} from "@heroicons/react/24/solid"; +import { SearchField, Spinner } from "@heroui/react"; +import { useQueryState } from "nuqs"; +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useLocation } from "wouter"; +import type { AvailableAgent } from "../contexts/agent-availability"; +import { useAgentAvailability } from "../hooks/use-agent-availability"; +import { + type SearchGroup, + type SearchMatch, + useGlobalSearch, +} from "../hooks/use-global-search"; +import { AgentIcon } from "../lib/agent-icons"; +import { cn } from "../lib/utils"; + +const PAGE_LIBRARY_LIMIT = 20; +const COLLAPSED_GROUP_LIMIT = 8; + +function ResultRow({ + match, + owningAgent, + onSelect, + isFirst, +}: { + match: SearchMatch; + owningAgent: AvailableAgent | null; + onSelect: (match: SearchMatch) => void; + isFirst: boolean; +}) { + return ( +
  • + +
  • + ); +} + +function ResultGroupSection({ + group, + agentLookup, + onSelect, +}: { + group: SearchGroup; + agentLookup: Map; + onSelect: (match: SearchMatch) => void; +}) { + const { t } = useTranslation(); + const [expanded, setExpanded] = useState(false); + + const totalCount = group.items.length; + const overflow = totalCount > COLLAPSED_GROUP_LIMIT; + const visibleItems = + expanded || !overflow + ? group.items + : group.items.slice(0, COLLAPSED_GROUP_LIMIT); + const hiddenCount = totalCount - visibleItems.length; + + return ( +
    +
    +

    + {t(group.labelKey)} +

    + {totalCount} +
    +
      + {visibleItems.map((match, idx) => { + const owningAgent = match.agentId + ? (agentLookup.get(match.agentId) ?? null) + : null; + return ( + + ); + })} + {overflow && ( +
    • + +
    • + )} +
    +
    + ); +} + +export default function SearchResultsPage() { + const { t } = useTranslation(); + const [, setLocation] = useLocation(); + const { availableAgents } = useAgentAvailability(); + const [urlQuery, setUrlQuery] = useQueryState("q", { defaultValue: "" }); + const [draft, setDraft] = useState(urlQuery); + + useEffect(() => { + setDraft(urlQuery); + }, [urlQuery]); + + const { groups, isMarketFetching } = useGlobalSearch({ + query: urlQuery, + libraryLimit: PAGE_LIBRARY_LIMIT, + }); + + const totalCount = useMemo( + () => groups.reduce((sum, g) => sum + g.items.length, 0), + [groups], + ); + + const agentLookup = useMemo(() => { + const map = new Map(); + for (const agent of availableAgents) { + map.set(agent.id, agent); + } + return map; + }, [availableAgents]); + + const handleSelect = (match: SearchMatch) => { + setLocation(match.href); + }; + + const trimmed = urlQuery.trim(); + + return ( +
    +
    +
    +
    + +

    + {t("searchResultsTitle")} +

    +
    + + + + { + if (event.key === "Enter") { + event.preventDefault(); + setUrlQuery(draft.trim() || null); + } + }} + /> + + + + {trimmed && ( +

    + {t("searchResultsSubtitle", { + count: totalCount, + query: trimmed, + })} +

    + )} +
    + + {!trimmed ? ( +
    + {t("searchResultsEmptyHint")} +
    + ) : groups.length === 0 && !isMarketFetching ? ( +
    + {t("globalSearchNoResults")} +
    + ) : ( +
    + {groups.map((group) => ( + + ))} + {isMarketFetching && ( +
    + + {t("globalSearchLibraryLoading")} +
    + )} +
    + )} +
    +
    + ); +} From b2cd51039f5ed99623c06fa4fe3a540ca38e19c6 Mon Sep 17 00:00:00 2001 From: danielchim Date: Sat, 9 May 2026 22:18:28 +0800 Subject: [PATCH 52/62] feat(desktop): add market page with skills/mcp/plugins/github tabs Introduces the unified `/market` discovery hub. The page is a single route with four tabs, switched via `?tab=`: - **Skills.sh** mounts the existing search-driven catalogue page. - **MCP Marketplace** shows a 12-card hardcoded grid of popular servers (filesystem, github, postgres, slack, puppeteer, memory, brave-search, linear, notion, sentry, stripe, google-drive) as a teaser. Each "Add manually" button jumps to /mcp. A preview callout at the top makes it clear this is not yet a live registry. - **Claude Code Plugins** shows a coming-soon placeholder with a link to the official plugin docs. - **Import from GitHub** mounts the existing import wizard panel. The page is unreachable until the App route table wires it up. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/desktop/src/pages/market.tsx | 364 ++++++++++++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 crates/desktop/src/pages/market.tsx diff --git a/crates/desktop/src/pages/market.tsx b/crates/desktop/src/pages/market.tsx new file mode 100644 index 00000000..e2cd2801 --- /dev/null +++ b/crates/desktop/src/pages/market.tsx @@ -0,0 +1,364 @@ +import { + BuildingStorefrontIcon, + CodeBracketIcon, + PuzzlePieceIcon, + ServerIcon, + SparklesIcon, +} from "@heroicons/react/24/solid"; +import { Button, Card, Surface, Tabs } from "@heroui/react"; +import { useQueryState } from "nuqs"; +import { useTranslation } from "react-i18next"; +import { useLocation } from "wouter"; +import { ImportGithubSkillPanel } from "../components/import-github-skill-panel"; +import { cn } from "../lib/utils"; +import SkillsShPage from "./skills-sh"; + +const MARKET_TAB_IDS = [ + "skills-sh", + "mcp", + "claude-plugins", + "github", +] as const; +type MarketTabId = (typeof MARKET_TAB_IDS)[number]; + +interface McpMarketEntry { + id: string; + name: string; + publisher: string; + description: string; + transport: "stdio" | "http"; + tags: string[]; +} + +const MOCK_MCP_SERVERS: McpMarketEntry[] = [ + { + id: "filesystem", + name: "Filesystem", + publisher: "modelcontextprotocol", + description: + "Read, write, and search files on the local filesystem with scoped permissions.", + transport: "stdio", + tags: ["official", "core"], + }, + { + id: "github", + name: "GitHub", + publisher: "modelcontextprotocol", + description: + "Browse repositories, issues, and PRs. Authenticated via personal access token.", + transport: "stdio", + tags: ["official", "git"], + }, + { + id: "postgres", + name: "Postgres", + publisher: "modelcontextprotocol", + description: + "Query Postgres databases with read-only or read-write modes.", + transport: "stdio", + tags: ["official", "database"], + }, + { + id: "slack", + name: "Slack", + publisher: "modelcontextprotocol", + description: "Read and post Slack messages, search threads.", + transport: "stdio", + tags: ["official", "communication"], + }, + { + id: "puppeteer", + name: "Puppeteer", + publisher: "modelcontextprotocol", + description: + "Headless browser automation: navigate, screenshot, scrape, and interact with pages.", + transport: "stdio", + tags: ["official", "browser"], + }, + { + id: "memory", + name: "Memory", + publisher: "modelcontextprotocol", + description: + "Persistent knowledge graph memory for cross-session context.", + transport: "stdio", + tags: ["official", "core"], + }, + { + id: "brave-search", + name: "Brave Search", + publisher: "modelcontextprotocol", + description: + "Web search via the Brave Search API. Free tier available.", + transport: "stdio", + tags: ["search"], + }, + { + id: "linear", + name: "Linear", + publisher: "linear", + description: + "Read and update Linear issues, projects, and cycles via API.", + transport: "http", + tags: ["productivity"], + }, + { + id: "notion", + name: "Notion", + publisher: "notion", + description: "Browse and update Notion pages, databases, and comments.", + transport: "http", + tags: ["productivity"], + }, + { + id: "sentry", + name: "Sentry", + publisher: "sentry", + description: + "Read errors, releases, and performance metrics from Sentry projects.", + transport: "http", + tags: ["observability"], + }, + { + id: "stripe", + name: "Stripe", + publisher: "stripe", + description: + "Inspect Stripe charges, subscriptions, and customers (read-only).", + transport: "http", + tags: ["payments"], + }, + { + id: "google-drive", + name: "Google Drive", + publisher: "modelcontextprotocol", + description: + "List, search, and read files from Google Drive (OAuth required).", + transport: "stdio", + tags: ["files"], + }, +]; + +function isMarketTabId(value: string | null): value is MarketTabId { + return MARKET_TAB_IDS.includes(value as MarketTabId); +} + +function McpMarketTab() { + const { t } = useTranslation(); + const [, setLocation] = useLocation(); + + return ( +
    + + + +
    +

    + {t("marketMcpComingSoonTitle")} +

    +

    + {t("marketMcpComingSoonHint")} +

    +
    +
    +
    + +
    + {MOCK_MCP_SERVERS.map((server) => ( + + +
    +
    + +
    +
    + + {server.name} + +

    + {server.publisher} +

    +
    +
    +
    + +

    + {server.description} +

    +
    + {server.tags.map((tag) => ( + + {tag} + + ))} + + {server.transport} + +
    + +
    +
    + ))} +
    +
    + ); +} + +function ClaudePluginsTab() { + const { t } = useTranslation(); + + const handleOpenDocs = () => { + window.open( + "https://docs.claude.com/en/docs/claude-code/plugins", + "_blank", + "noopener,noreferrer", + ); + }; + + return ( +
    + + +
    + +
    +
    +

    + {t("marketClaudePluginsComingSoonTitle")} +

    +

    + {t("marketClaudePluginsComingSoonHint")} +

    +
    + +
    +
    +
    + ); +} + +export default function MarketPage() { + const { t } = useTranslation(); + const [tabParam, setTabParam] = useQueryState("tab", { + defaultValue: "skills-sh", + }); + const activeTab: MarketTabId = isMarketTabId(tabParam) + ? tabParam + : "skills-sh"; + + return ( +
    +
    +
    + +
    +
    +

    + {t("market")} +

    +

    + {t("marketSubtitle")} +

    +
    +
    + + + + setTabParam(String(key) as MarketTabId) + } + > + + + + + + {t("marketTabSkillsSh")} + + + + + + + {t("marketTabMcp")} + + + + + + + {t("marketTabClaudePlugins")} + + + + + + + {t("marketTabGithub")} + + + + + + + + +
    + {activeTab === "skills-sh" && } + {activeTab === "mcp" && ( +
    + +
    + )} + {activeTab === "claude-plugins" && } + {activeTab === "github" && ( + {}} /> + )} +
    +
    + ); +} From c30af953e9c8e329aec0ab31e56ea048d62462d2 Mon Sep 17 00:00:00 2001 From: danielchim Date: Sat, 9 May 2026 22:19:08 +0800 Subject: [PATCH 53/62] feat(desktop): rebuild sidebar IA, route table, and locales for agent hub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pivots the product from a flat resource manager into an agent-first hub. The sidebar now reads top-down as Search → Home/Market → All Resources → All Agents → Projects → Settings, with a fixed search at the top, a single scrollable region in the middle, and Settings pinned at the bottom. Sidebar structure: - Search box mounts the new component. - Two top-level destinations (Home, Market) sit above the first separator. - "All Resources" exposes the existing global views — Skills, MCP Servers, Sub-agents, Inference Providers — for power users who want a cross-agent radar. - "All Agents" lists installed agents only (filtered by availability.is_available, disabled agents pushed to the bottom). Long lists collapse to 5 with a "Show N more" toggle that always keeps the active agent pinned visible. - Project list now follows the same section-label spacing and no longer has a stray mt-4 leftover from the pre-separator layout. Routes: - New: /, /agents/:agentId/:rest*, /search, /market, /market/search. - Cross-agent views remain reachable: /skills, /mcp, /sub-agents, /inference-providers all stay alive (no longer redirect to /). - /library and /skills-sh redirect to /market and /market/search; in-app links in skill-detail, menu, home, and skills-sh pages now point at /market. Store: - SidebarItemId shrinks from five resource ids to two static ids ("home" | "market"). v6→v7 wipes legacy preferences; v7→v8 swaps "library" for "market". CURRENT_VERSION bumped to 8. - use-sidebar-navigation drops the visibility/order mutation surface — the static items are no longer user-reorderable. Other cleanup: - Deletes the unused settings/sidebar-panel.tsx and removes its link from appearance-panel. - Adds 30+ i18n keys across en/zh-Hans/zh-Hant covering Home, Market, search, agent statuses, model-not-configurable hints, and the open-config-folder action. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/desktop/src/App.tsx | 154 ++++++---- crates/desktop/src/components/app-sidebar.tsx | 276 +++++++++++++++--- .../desktop/src/components/project-list.tsx | 4 +- .../desktop/src/components/skill-detail.tsx | 4 +- .../src/hooks/use-sidebar-navigation.ts | 108 +------ crates/desktop/src/lib/locales/en.ts | 60 ++++ crates/desktop/src/lib/locales/zh-Hans.ts | 57 ++++ crates/desktop/src/lib/locales/zh-Hant.ts | 57 ++++ crates/desktop/src/lib/menu.ts | 2 +- crates/desktop/src/lib/sidebar-navigation.ts | 64 +--- .../desktop/src/lib/store/migrations/index.ts | 10 + .../src/lib/store/migrations/v6-to-v7.ts | 6 + .../src/lib/store/migrations/v7-to-v8.ts | 6 + crates/desktop/src/lib/store/types.ts | 11 +- .../src/pages/settings/appearance-panel.tsx | 3 - .../src/pages/settings/sidebar-panel.tsx | 173 ----------- crates/desktop/src/pages/skills-sh/index.tsx | 2 +- crates/desktop/src/pages/skills-sh/search.tsx | 2 +- 18 files changed, 550 insertions(+), 449 deletions(-) create mode 100644 crates/desktop/src/lib/store/migrations/v6-to-v7.ts create mode 100644 crates/desktop/src/lib/store/migrations/v7-to-v8.ts delete mode 100644 crates/desktop/src/pages/settings/sidebar-panel.tsx diff --git a/crates/desktop/src/App.tsx b/crates/desktop/src/App.tsx index 999303e8..70c11f33 100644 --- a/crates/desktop/src/App.tsx +++ b/crates/desktop/src/App.tsx @@ -14,21 +14,23 @@ import { DeepLinkImportModal } from "./components/deep-link-import-modal"; import { OnboardingController } from "./components/onboarding-controller"; import { Redirect } from "./components/redirect"; import { ErrorBoundary } from "./components/ui/error-boundary"; -import { useSidebarNavigation } from "./hooks/use-sidebar-navigation"; import { MainLayout } from "./layouts/main-layout"; import type { DeepLinkImportIntent } from "./lib/deep-link"; import { parseDeepLink } from "./lib/deep-link"; import { setupAppMenu } from "./lib/menu"; import { initStore } from "./lib/store"; +import AgentDetailPage from "./pages/agent"; +import HomePage from "./pages/home"; import InferenceProvidersPage from "./pages/inference-providers"; +import MarketPage from "./pages/market"; import PluginsPage from "./pages/plugins"; import ProjectDetailPage from "./pages/project/detail"; +import SearchResultsPage from "./pages/search"; import SettingsPage from "./pages/settings"; import CustomAgentsPage from "./pages/settings/custom-agents"; import MCPServersPage from "./pages/settings/mcp-servers"; import SkillsPage from "./pages/settings/skills"; import SubAgentsPage from "./pages/settings/sub-agents"; -import SkillsShPage from "./pages/skills-sh"; import SkillsSearchPage from "./pages/skills-sh/search"; import { AgentAvailabilityProvider } from "./providers/agent-availability"; import { ServerProvider } from "./providers/server"; @@ -44,31 +46,14 @@ const queryClient = new QueryClient({ }, }); -function SkillsPageSkeleton() { +function PageSkeleton() { return ( -
    -
    - -
    -
    +
    +
    ); } -function DefaultSidebarRoute() { - const { defaultHref, isLoading } = useSidebarNavigation(); - - if (isLoading) { - return null; - } - - return ; -} - function App() { const [isStoreReady, setIsStoreReady] = useState(false); const [pendingIntents, setPendingIntents] = useState< @@ -178,110 +163,161 @@ function App() { - - - - } + fallback={} > - + - + + - } + fallback={} > - + - + + - + } + > + + - + + - } + fallback={} > - + - + + - } + fallback={} > - + - + + - } + fallback={} > - + - + + + + + + + + + - + + } + > + + + - + - + + } + > + + + - } + fallback={} > + + + + + + + + + + + + + + + + + + } + > + + + + + + + + + + + + + + + + + - +
    -