//! The client's RPC surface: every command, wired to its Tauri implementation. //! //! The *shape* of the surface — command names, request payloads, response //! types — is declared once in `yaak_rpc_schema`, which is also where the //! TypeScript bindings come from. This module supplies the desktop's half: an //! adapter per command that turns a `ClientCtx` and a request struct into a //! call on the existing implementation, and the single `rpc` Tauri command that //! is the only way the frontend reaches any of it — one envelope, //! `{ cmd, payload }`, exactly like the proxy app. //! //! Command bodies live in one of two places. Host-independent ones are in //! `yaak_commands`, written against its `Host` trait, which `ClientCtx` //! implements below; their adapters are one line. The rest still have their //! natural Tauri signatures (window, app handle, managed state) and their //! adapters unpack the request for them. Either way the wire format stays //! transport-agnostic: another host builds its router from the same schema //! with its own `Host`, and the frontend cannot tell. use crate::error::Result; use crate::notifications::YaakNotifier; use crate::updates::YaakUpdater; use log::warn; use serde::Serialize; use std::collections::HashMap; use std::sync::Arc; use tauri::{Manager, Runtime, State, WebviewWindow}; use tokio::sync::Mutex; use yaak_commands::{Host, PluginHost}; use yaak_core::WorkspaceContext; use yaak_crypto::manager::EncryptionManager; use yaak_git::{ BranchDeleteResult, CloneResult, GitBranchInfo, GitCommit, GitFileDiff, GitRemote, GitStatusSummary, GitWorktreeStatus, PullResult, PushResult, }; use yaak_grpc::ServiceDefinition; use yaak_grpc::manager::GrpcHandle; use yaak_models::blob_manager::BlobManager; use yaak_models::models::{ GraphQlIntrospection, GrpcEvent, HttpRequest, HttpRequestHeader, HttpResponse, HttpResponseEvent, ImportSource, Plugin, Settings, WebsocketConnection, WebsocketEvent, WorkspaceMeta, }; use yaak_models::query_manager::QueryManager; use yaak_models::util::{BatchUpsertResult, ImportPlan}; use yaak_plugins::api::{PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse}; use yaak_plugins::events::{ CallFolderActionRequest, CallGrpcRequestActionRequest, CallHttpRequestActionRequest, CallWebsocketRequestActionRequest, CallWorkspaceActionRequest, FilterResponse, GetFolderActionsResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse, GetThemesResponse, GetWebsocketRequestActionsResponse, GetWorkspaceActionsResponse, ImportResponse, JsonPrimitive, RenderPurpose, }; use yaak_plugins::manager::PluginManager; use yaak_plugins::native_template_functions::encrypt_secure_template_function; use yaak_plugins::plugin_meta::PluginMetadata; use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_rpc::RpcRouter; use yaak_rpc_schema::*; use yaak_sse::sse::ServerSentEvent; use yaak_sync::sync::SyncOp; use yaak_tauri_utils::window::WorkspaceWindowTrait; use yaak_templates::TemplateCallback; use yaak_ws::WebsocketManager; /// Per-call context: the window a command was invoked from. /// /// Window identity is load-bearing — model writes carry it for echo /// suppression, and plugin events and toasts are routed back through it — so it /// rides along with every dispatch rather than living in shared state. pub(crate) struct ClientCtx { pub window: WebviewWindow, } // Derived Clone would demand `R: Clone`, which `Runtime` types don't provide; // the window handle itself is always cloneable. impl Clone for ClientCtx { fn clone(&self) -> Self { Self { window: self.window.clone() } } } /// The desktop is a host: the client is the window, the session is the /// window's URL, and the shared managers are Tauri managed state. impl Host for ClientCtx { fn client_id(&self) -> &str { self.window.label() } fn session(&self) -> WorkspaceContext { self.window.workspace_context() } fn app_version(&self) -> String { self.window.package_info().version.to_string() } fn query_manager(&self) -> &QueryManager { self.window.state::().inner() } fn blob_manager(&self) -> &BlobManager { self.window.state::().inner() } fn encryption_manager(&self) -> &EncryptionManager { self.window.state::().inner() } } impl ClientCtx { /// The plugin runtime this window talks to, once it finishes booting. /// Only the `PluginHost` impl below uses it; everything else goes through /// the trait. async fn pm(&self) -> yaak_plugins::error::Result { crate::plugins_ext::plugin_manager(&self.window).await } } /// The desktop answers all of these out of the `PluginManager` it already /// runs — the Node sidecar. Each is a delegation, which is the point: the /// operations are what the handlers need, and this is one host's way of /// providing them. impl PluginHost for ClientCtx { async fn loaded_plugin_metadata(&self, directory: &str) -> Option { let handle = self.pm().await.ok()?.get_plugin_by_dir(directory).await?; Some(handle.info()) } async fn take_plugin_init_errors(&self) -> Vec<(String, String)> { match self.pm().await { Ok(pm) => pm.take_init_errors().await, Err(_) => Vec::new(), } } async fn resolve_plugins(&self, plugins: Vec) -> Vec { match self.pm().await { Ok(pm) => pm.resolve_plugins_for_runtime_from_db(plugins).await, Err(_) => plugins, } } async fn template_callback( &self, purpose: RenderPurpose, ) -> yaak_commands::Result { Ok(PluginTemplateCallback::new( Arc::new(self.pm().await?), Arc::new(self.encryption_manager().clone()), &self.plugin_context(), purpose, )) } async fn template_function_summaries( &self, ) -> yaak_commands::Result> { Ok(self.pm().await?.get_template_function_summaries(&self.plugin_context()).await?) } async fn template_function_config( &self, function_name: &str, values: HashMap, model_id: &str, ) -> yaak_commands::Result { Ok(self .pm() .await? .get_template_function_config(&self.plugin_context(), function_name, values, model_id) .await?) } async fn themes(&self) -> yaak_commands::Result> { Ok(self.pm().await?.get_themes(&self.plugin_context()).await?) } async fn http_request_actions( &self, ) -> yaak_commands::Result> { Ok(self.pm().await?.get_http_request_actions(&self.plugin_context()).await?) } async fn websocket_request_actions( &self, ) -> yaak_commands::Result> { Ok(self.pm().await?.get_websocket_request_actions(&self.plugin_context()).await?) } async fn grpc_request_actions( &self, ) -> yaak_commands::Result> { Ok(self.pm().await?.get_grpc_request_actions(&self.plugin_context()).await?) } async fn workspace_actions(&self) -> yaak_commands::Result> { Ok(self.pm().await?.get_workspace_actions(&self.plugin_context()).await?) } async fn folder_actions(&self) -> yaak_commands::Result> { Ok(self.pm().await?.get_folder_actions(&self.plugin_context()).await?) } async fn call_http_request_action( &self, req: CallHttpRequestActionRequest, ) -> yaak_commands::Result<()> { Ok(self.pm().await?.call_http_request_action(&self.plugin_context(), req).await?) } async fn call_grpc_request_action( &self, req: CallGrpcRequestActionRequest, ) -> yaak_commands::Result<()> { Ok(self.pm().await?.call_grpc_request_action(&self.plugin_context(), req).await?) } async fn call_websocket_request_action( &self, req: CallWebsocketRequestActionRequest, ) -> yaak_commands::Result<()> { Ok(self.pm().await?.call_websocket_request_action(&self.plugin_context(), req).await?) } async fn call_workspace_action( &self, req: CallWorkspaceActionRequest, ) -> yaak_commands::Result<()> { Ok(self.pm().await?.call_workspace_action(&self.plugin_context(), req).await?) } async fn call_folder_action(&self, req: CallFolderActionRequest) -> yaak_commands::Result<()> { Ok(self.pm().await?.call_folder_action(&self.plugin_context(), req).await?) } async fn http_authentication_summaries( &self, ) -> yaak_commands::Result> { let results = self.pm().await?.get_http_authentication_summaries(&self.plugin_context()).await?; Ok(results.into_iter().map(|(_, a)| a).collect()) } async fn http_authentication_config( &self, auth_name: &str, values: HashMap, model_id: &str, ) -> yaak_commands::Result { Ok(self .pm() .await? .get_http_authentication_config(&self.plugin_context(), auth_name, values, model_id) .await?) } async fn call_http_authentication_action( &self, auth_name: &str, action_index: i32, values: HashMap, model_id: &str, ) -> yaak_commands::Result<()> { Ok(self .pm() .await? .call_http_authentication_action( &self.plugin_context(), auth_name, action_index, values, model_id, ) .await?) } async fn import_data(&self, content: &str) -> yaak_commands::Result { Ok(self.pm().await?.import_data(&self.plugin_context(), content).await?) } async fn reload_plugins(&self, plugins: Vec) -> Vec<(String, String)> { match self.pm().await { Ok(pm) => pm.initialize_all_plugins(plugins, &self.plugin_context()).await, Err(e) => vec![("*".to_string(), e.to_string())], } } async fn encrypt_secure_template(&self, template: &str) -> yaak_commands::Result { let plugin_manager = Arc::new(self.pm().await?); let encryption_manager = Arc::new(self.encryption_manager().clone()); Ok(encrypt_secure_template_function( plugin_manager, encryption_manager, &self.plugin_context(), template, )?) } } /// The one Tauri command. The payload is the yaak-rpc envelope's payload; /// a missing payload means an empty one. #[tauri::command] pub(crate) async fn rpc( window: WebviewWindow, router: State<'_, RpcRouter>>, cmd: String, payload: Option, ) -> std::result::Result { let ctx = ClientCtx { window }; let payload = payload.unwrap_or_else(|| serde_json::Value::Object(Default::default())); log::debug!("RPC {cmd}"); router.dispatch(&cmd, payload, &ctx).await.map_err(|e| { log::warn!("RPC {cmd} failed: {}", e.message); e.message }) } /// A callback that forwards a stream of messages to the calling window as /// `stream_{id}` events. The stream id is minted by the caller so it can /// subscribe before the command starts and never miss a message. fn stream_emitter( ctx: &ClientCtx, stream_id: &str, ) -> impl Fn(T) + Send + Sync + 'static { use tauri::Emitter; let window = ctx.window.clone(); let event = format!("stream_{stream_id}"); move |payload: T| { let value = match serde_json::to_value(payload) { Ok(value) => value, Err(e) => { warn!("Failed to serialize stream event: {e}"); return; } }; if let Err(e) = window.emit_to(window.label(), &event, value) { warn!("Failed to emit stream event: {e}"); } } } // -- Streaming commands (hand-written: they push events while they run) -- async fn cmd_git_watch_worktree_status( ctx: ClientCtx, req: CmdGitWatchWorktreeStatusReq, ) -> Result { let on_status = stream_emitter(&ctx, &req.stream_id); crate::git_watcher::watch_git_worktree_status( ctx.window.app_handle().clone(), &req.dir, on_status, ) .await } async fn cmd_sync_watch( ctx: ClientCtx, req: CmdSyncWatchReq, ) -> Result { let on_event = stream_emitter(&ctx, &req.stream_id); Ok(crate::sync_ext::sync_watch( ctx.window.app_handle().clone(), &req.sync_dir, &req.workspace_id, on_event, ) .await?) } /// Build the router with every command in the schema registered to its /// adapter here. Generic over the runtime so the same registry serves Wry and /// CEF builds. Adding a command means adding it to the schema *and* writing an /// adapter below; a missing adapter fails to compile rather than 404 at runtime. macro_rules! register_commands { ( $( $name:ident ( $req:ty ) -> $res:ty ),* $(,)? ) => { pub(crate) fn build_rpc_router() -> RpcRouter> { let mut router = RpcRouter::new(); $( router.register(stringify!($name), yaak_rpc::rpc_handler_async!($name)); )* router } }; } yaak_rpc_schema::with_commands!(register_commands); // -- Adapters -- async fn cmd_metadata(ctx: ClientCtx, _req: CmdMetadataReq) -> Result { Ok(crate::cmd_metadata(ctx.window.app_handle().clone()).await?) } async fn cmd_template_tokens_to_string( ctx: ClientCtx, req: CmdTemplateTokensToStringReq, ) -> Result { Ok(yaak_commands::templates::cmd_template_tokens_to_string(ctx, req).await?) } async fn cmd_render_template( ctx: ClientCtx, req: CmdRenderTemplateReq, ) -> Result { Ok(yaak_commands::templates::cmd_render_template(ctx, req).await?) } async fn cmd_send_feedback(ctx: ClientCtx, req: CmdSendFeedbackReq) -> Result<()> { Ok(crate::cmd_send_feedback(ctx.window.app_handle().clone(), req.feature, req.text).await?) } async fn cmd_dismiss_notification( ctx: ClientCtx, req: CmdDismissNotificationReq, ) -> Result<()> { Ok(crate::cmd_dismiss_notification( ctx.window.clone(), &req.notification_id, ctx.window.app_handle().state::>(), ) .await?) } async fn cmd_grpc_reflect( ctx: ClientCtx, req: CmdGrpcReflectReq, ) -> Result> { Ok(crate::cmd_grpc_reflect( &req.request_id, req.environment_id.as_deref(), req.proto_files, ctx.window.clone(), ctx.window.app_handle().clone(), ctx.window.app_handle().state::>(), ) .await?) } async fn cmd_grpc_go(ctx: ClientCtx, req: CmdGrpcGoReq) -> Result { Ok(crate::cmd_grpc_go( &req.request_id, req.environment_id.as_deref(), req.proto_files, ctx.window.app_handle().clone(), ctx.window.clone(), ctx.window.app_handle().state::>(), ) .await?) } async fn cmd_restart(ctx: ClientCtx, _req: CmdRestartReq) -> Result<()> { Ok(crate::cmd_restart(ctx.window.app_handle().clone()).await?) } async fn cmd_send_ephemeral_request( ctx: ClientCtx, req: CmdSendEphemeralRequestReq, ) -> Result { Ok(crate::cmd_send_ephemeral_request( req.request, req.environment_id.as_deref(), req.cookie_jar_id.as_deref(), ctx.window.clone(), ctx.window.app_handle().clone(), ) .await?) } async fn cmd_format_json(ctx: ClientCtx, req: CmdFormatJsonReq) -> Result { Ok(yaak_commands::data::cmd_format_json(ctx, req).await?) } async fn cmd_format_graphql( _ctx: ClientCtx, req: CmdFormatGraphqlReq, ) -> Result { Ok(crate::cmd_format_graphql(&req.text).await?) } async fn cmd_http_response_body( ctx: ClientCtx, req: CmdHttpResponseBodyReq, ) -> Result { Ok(crate::cmd_http_response_body(ctx.window.clone(), &req.response_id, req.filter.as_deref()) .await?) } async fn cmd_http_response_body_path( ctx: ClientCtx, req: CmdHttpResponseBodyPathReq, ) -> Result> { Ok(yaak_commands::responses::cmd_http_response_body_path(ctx, req).await?) } async fn cmd_http_request_body( ctx: ClientCtx, req: CmdHttpRequestBodyReq, ) -> Result>> { Ok(yaak_commands::responses::cmd_http_request_body(ctx, req).await?) } async fn cmd_get_sse_events( ctx: ClientCtx, req: CmdGetSseEventsReq, ) -> Result> { Ok(crate::cmd_get_sse_events(ctx.window.app_handle().clone(), &req.response_id).await?) } async fn cmd_get_http_response_events( ctx: ClientCtx, req: CmdGetHttpResponseEventsReq, ) -> Result> { Ok(yaak_commands::responses::cmd_get_http_response_events(ctx, req).await?) } async fn cmd_import_data( ctx: ClientCtx, req: CmdImportDataReq, ) -> Result { Ok(crate::cmd_import_data(ctx.window.clone(), &req.file_path, req.destination).await?) } async fn cmd_import_url(ctx: ClientCtx, req: CmdImportUrlReq) -> Result { Ok(crate::cmd_import_url(ctx.window.clone(), &req.url, req.destination).await?) } async fn cmd_commit_import( ctx: ClientCtx, req: CmdCommitImportReq, ) -> Result { Ok(crate::cmd_commit_import(ctx.window.clone(), req.plan).await?) } async fn cmd_list_import_sources( ctx: ClientCtx, req: CmdListImportSourcesReq, ) -> Result> { use crate::models_ext::QueryManagerExt; Ok(ctx.window.db().list_import_sources(&req.workspace_id)?) } async fn cmd_import_sources_for_origin( ctx: ClientCtx, req: CmdImportSourcesForOriginReq, ) -> Result> { use crate::models_ext::QueryManagerExt; let origin = match (req.file_path, req.url) { (Some(file_path), _) => crate::import::file_origin(&file_path).origin, (None, Some(url)) => match crate::import::normalize_import_url(&url) { Ok(url) => crate::import::url_origin(&url).origin, Err(_) => return Ok(Vec::new()), }, (None, None) => return Ok(Vec::new()), }; Ok(ctx.window.db().list_import_sources_by_origin(&origin)?) } async fn cmd_http_request_actions( ctx: ClientCtx, req: CmdHttpRequestActionsReq, ) -> Result> { Ok(yaak_commands::actions::cmd_http_request_actions(ctx, req).await?) } async fn cmd_websocket_request_actions( ctx: ClientCtx, req: CmdWebsocketRequestActionsReq, ) -> Result> { Ok(yaak_commands::actions::cmd_websocket_request_actions(ctx, req).await?) } async fn cmd_call_websocket_request_action( ctx: ClientCtx, req: CmdCallWebsocketRequestActionReq, ) -> Result<()> { Ok(yaak_commands::actions::cmd_call_websocket_request_action(ctx, req).await?) } async fn cmd_workspace_actions( ctx: ClientCtx, req: CmdWorkspaceActionsReq, ) -> Result> { Ok(yaak_commands::actions::cmd_workspace_actions(ctx, req).await?) } async fn cmd_call_workspace_action( ctx: ClientCtx, req: CmdCallWorkspaceActionReq, ) -> Result<()> { Ok(yaak_commands::actions::cmd_call_workspace_action(ctx, req).await?) } async fn cmd_folder_actions( ctx: ClientCtx, req: CmdFolderActionsReq, ) -> Result> { Ok(yaak_commands::actions::cmd_folder_actions(ctx, req).await?) } async fn cmd_call_folder_action( ctx: ClientCtx, req: CmdCallFolderActionReq, ) -> Result<()> { Ok(yaak_commands::actions::cmd_call_folder_action(ctx, req).await?) } async fn cmd_grpc_request_actions( ctx: ClientCtx, req: CmdGrpcRequestActionsReq, ) -> Result> { Ok(yaak_commands::actions::cmd_grpc_request_actions(ctx, req).await?) } async fn cmd_template_function_summaries( ctx: ClientCtx, req: CmdTemplateFunctionSummariesReq, ) -> Result> { Ok(yaak_commands::templates::cmd_template_function_summaries(ctx, req).await?) } async fn cmd_template_function_config( ctx: ClientCtx, req: CmdTemplateFunctionConfigReq, ) -> Result { Ok(yaak_commands::templates::cmd_template_function_config(ctx, req).await?) } async fn cmd_get_http_authentication_summaries( ctx: ClientCtx, req: CmdGetHttpAuthenticationSummariesReq, ) -> Result> { Ok(yaak_commands::auth::cmd_get_http_authentication_summaries(ctx, req).await?) } async fn cmd_get_http_authentication_config( ctx: ClientCtx, req: CmdGetHttpAuthenticationConfigReq, ) -> Result { Ok(yaak_commands::auth::cmd_get_http_authentication_config(ctx, req).await?) } async fn cmd_call_http_request_action( ctx: ClientCtx, req: CmdCallHttpRequestActionReq, ) -> Result<()> { Ok(yaak_commands::actions::cmd_call_http_request_action(ctx, req).await?) } async fn cmd_call_grpc_request_action( ctx: ClientCtx, req: CmdCallGrpcRequestActionReq, ) -> Result<()> { Ok(yaak_commands::actions::cmd_call_grpc_request_action(ctx, req).await?) } async fn cmd_call_http_authentication_action( ctx: ClientCtx, req: CmdCallHttpAuthenticationActionReq, ) -> Result<()> { Ok(yaak_commands::auth::cmd_call_http_authentication_action(ctx, req).await?) } async fn cmd_curl_to_request( ctx: ClientCtx, req: CmdCurlToRequestReq, ) -> Result { Ok(yaak_commands::actions::cmd_curl_to_request(ctx, req).await?) } async fn cmd_export_data(ctx: ClientCtx, req: CmdExportDataReq) -> Result<()> { Ok(yaak_commands::data::cmd_export_data(ctx, req).await?) } async fn cmd_save_base64_to_binary( ctx: ClientCtx, req: CmdSaveBase64ToBinaryReq, ) -> Result<()> { Ok(crate::cmd_save_base64_to_binary(ctx.window.app_handle().clone(), &req.filepath, &req.data) .await?) } async fn cmd_save_response(ctx: ClientCtx, req: CmdSaveResponseReq) -> Result<()> { Ok(yaak_commands::responses::cmd_save_response(ctx, req).await?) } async fn cmd_send_http_request( ctx: ClientCtx, req: CmdSendHttpRequestReq, ) -> Result { Ok(crate::cmd_send_http_request( ctx.window.app_handle().clone(), ctx.window.clone(), req.environment_id.as_deref(), req.cookie_jar_id.as_deref(), req.request_id, ) .await?) } async fn cmd_reload_plugins( ctx: ClientCtx, req: CmdReloadPluginsReq, ) -> Result> { Ok(yaak_commands::actions::cmd_reload_plugins(ctx, req).await?) } async fn cmd_plugin_info( ctx: ClientCtx, req: CmdPluginInfoReq, ) -> Result { Ok(yaak_commands::plugins::cmd_plugin_info(ctx, req).await?) } async fn cmd_delete_all_grpc_connections( ctx: ClientCtx, req: CmdDeleteAllGrpcConnectionsReq, ) -> Result<()> { Ok(yaak_commands::models::cmd_delete_all_grpc_connections(ctx, req).await?) } async fn cmd_delete_send_history( ctx: ClientCtx, req: CmdDeleteSendHistoryReq, ) -> Result<()> { Ok(yaak_commands::models::cmd_delete_send_history(ctx, req).await?) } async fn cmd_delete_all_http_responses( ctx: ClientCtx, req: CmdDeleteAllHttpResponsesReq, ) -> Result<()> { Ok(yaak_commands::models::cmd_delete_all_http_responses(ctx, req).await?) } async fn cmd_get_workspace_meta( ctx: ClientCtx, req: CmdGetWorkspaceMetaReq, ) -> Result { Ok(yaak_commands::models::cmd_get_workspace_meta(ctx, req).await?) } async fn cmd_new_child_window( ctx: ClientCtx, req: CmdNewChildWindowReq, ) -> Result<()> { Ok(crate::cmd_new_child_window( ctx.window.clone(), &req.url, &req.label, &req.title, req.inner_size, ) .await?) } async fn cmd_new_main_window( ctx: ClientCtx, req: CmdNewMainWindowReq, ) -> Result<()> { Ok(crate::cmd_new_main_window(ctx.window.app_handle().clone(), &req.url).await?) } async fn cmd_check_for_updates( ctx: ClientCtx, _req: CmdCheckForUpdatesReq, ) -> Result { Ok(crate::cmd_check_for_updates( ctx.window.clone(), ctx.window.app_handle().state::>(), ) .await?) } async fn cmd_decrypt_template( ctx: ClientCtx, req: CmdDecryptTemplateReq, ) -> Result { Ok(yaak_commands::encryption::cmd_decrypt_template(ctx, req).await?) } async fn cmd_secure_template( ctx: ClientCtx, req: CmdSecureTemplateReq, ) -> Result { Ok(yaak_commands::encryption::cmd_secure_template(ctx, req).await?) } async fn cmd_get_themes( ctx: ClientCtx, req: CmdGetThemesReq, ) -> Result> { Ok(yaak_commands::templates::cmd_get_themes(ctx, req).await?) } async fn cmd_enable_encryption( ctx: ClientCtx, req: CmdEnableEncryptionReq, ) -> Result<()> { Ok(yaak_commands::encryption::cmd_enable_encryption(ctx, req).await?) } async fn cmd_reveal_workspace_key( ctx: ClientCtx, req: CmdRevealWorkspaceKeyReq, ) -> Result { Ok(yaak_commands::encryption::cmd_reveal_workspace_key(ctx, req).await?) } async fn cmd_set_workspace_key( ctx: ClientCtx, req: CmdSetWorkspaceKeyReq, ) -> Result<()> { Ok(yaak_commands::encryption::cmd_set_workspace_key(ctx, req).await?) } async fn cmd_disable_encryption( ctx: ClientCtx, req: CmdDisableEncryptionReq, ) -> Result<()> { Ok(yaak_commands::encryption::cmd_disable_encryption(ctx, req).await?) } async fn cmd_default_headers( ctx: ClientCtx, req: CmdDefaultHeadersReq, ) -> Result> { Ok(yaak_commands::models::cmd_default_headers(ctx, req).await?) } async fn models_upsert(ctx: ClientCtx, req: ModelsUpsertReq) -> Result { Ok(yaak_commands::models::models_upsert(ctx, req).await?) } /// Runs on a blocking thread rather than the async runtime: a cascading delete /// (a workspace with thousands of requests) holds a transaction for its whole /// duration, and stalling the runtime stalls every other IPC call behind it. /// That is this host's concern, so the shared handler stays plain and the /// relocation happens here. async fn models_delete(ctx: ClientCtx, req: ModelsDeleteReq) -> Result { let deleted = tauri::async_runtime::spawn_blocking(move || { yaak_commands::models::models_delete_blocking(&ctx, req) }) .await .map_err(|e| crate::error::Error::GenericError(format!("Delete task failed: {e}")))?; Ok(deleted?) } async fn models_duplicate( ctx: ClientCtx, req: ModelsDuplicateReq, ) -> Result { Ok(yaak_commands::models::models_duplicate(ctx, req).await?) } async fn models_websocket_events( ctx: ClientCtx, req: ModelsWebsocketEventsReq, ) -> Result> { Ok(yaak_commands::models::models_websocket_events(ctx, req).await?) } async fn models_grpc_events( ctx: ClientCtx, req: ModelsGrpcEventsReq, ) -> Result> { Ok(yaak_commands::models::models_grpc_events(ctx, req).await?) } async fn models_get_settings( ctx: ClientCtx, req: ModelsGetSettingsReq, ) -> Result { Ok(yaak_commands::models::models_get_settings(ctx, req).await?) } async fn models_get_graphql_introspection( ctx: ClientCtx, req: ModelsGetGraphqlIntrospectionReq, ) -> Result> { Ok(yaak_commands::models::models_get_graphql_introspection(ctx, req).await?) } async fn models_upsert_graphql_introspection( ctx: ClientCtx, req: ModelsUpsertGraphqlIntrospectionReq, ) -> Result { Ok(yaak_commands::models::models_upsert_graphql_introspection(ctx, req).await?) } /// Non-ASCII is escaped to `\uXXXX` before the JSON crosses into the webview: /// on Linux, sending Cyrillic (and possibly other) characters through this /// payload leaves every string in the parsed models subtly mis-encoded and /// CodeMirror unable to place the cursor (feedback: "editing the URL sometimes /// freezes the app"). Escape sequences sidestep it. This is a quirk of the /// webview transport, not of the data, so it lives in the adapter rather than /// the shared handler. async fn models_workspace_models( ctx: ClientCtx, req: ModelsWorkspaceModelsReq, ) -> Result { let json = yaak_commands::models::models_workspace_models(ctx, req).await?; Ok(escape_str_for_webview(&json)) } fn escape_str_for_webview(input: &str) -> String { input .chars() .map(|c| { let code = c as u32; // ASCII if code <= 0x7F { c.to_string() // BMP characters encoded normally } else if code < 0xFFFF { format!("\\u{:04X}", code) // Beyond BMP encoded a surrogate pairs } else { let high = ((code - 0x10000) >> 10) + 0xD800; let low = ((code - 0x10000) & 0x3FF) + 0xDC00; format!("\\u{:04X}\\u{:04X}", high, low) } }) .collect() } async fn cmd_git_checkout( _ctx: ClientCtx, req: CmdGitCheckoutReq, ) -> Result { Ok(crate::git_ext::cmd_git_checkout(&req.dir, &req.branch, req.force).await?) } async fn cmd_git_branch(_ctx: ClientCtx, req: CmdGitBranchReq) -> Result<()> { Ok(crate::git_ext::cmd_git_branch(&req.dir, &req.branch, req.base.as_deref()).await?) } async fn cmd_git_delete_branch( _ctx: ClientCtx, req: CmdGitDeleteBranchReq, ) -> Result { Ok(crate::git_ext::cmd_git_delete_branch(&req.dir, &req.branch, req.force).await?) } async fn cmd_git_delete_remote_branch( _ctx: ClientCtx, req: CmdGitDeleteRemoteBranchReq, ) -> Result<()> { Ok(crate::git_ext::cmd_git_delete_remote_branch(&req.dir, &req.branch).await?) } async fn cmd_git_merge_branch( _ctx: ClientCtx, req: CmdGitMergeBranchReq, ) -> Result<()> { Ok(crate::git_ext::cmd_git_merge_branch(&req.dir, &req.branch).await?) } async fn cmd_git_rename_branch( _ctx: ClientCtx, req: CmdGitRenameBranchReq, ) -> Result<()> { Ok(crate::git_ext::cmd_git_rename_branch(&req.dir, &req.old_name, &req.new_name).await?) } async fn cmd_git_status( _ctx: ClientCtx, req: CmdGitStatusReq, ) -> Result { Ok(crate::git_ext::cmd_git_status(&req.dir).await?) } async fn cmd_git_branch_info( _ctx: ClientCtx, req: CmdGitBranchInfoReq, ) -> Result { Ok(crate::git_ext::cmd_git_branch_info(&req.dir).await?) } async fn cmd_git_worktree_status( _ctx: ClientCtx, req: CmdGitWorktreeStatusReq, ) -> Result { Ok(crate::git_ext::cmd_git_worktree_status(&req.dir).await?) } async fn cmd_git_log(_ctx: ClientCtx, req: CmdGitLogReq) -> Result> { Ok(crate::git_ext::cmd_git_log(&req.dir).await?) } async fn cmd_git_log_for_file( _ctx: ClientCtx, req: CmdGitLogForFileReq, ) -> Result> { Ok(crate::git_ext::cmd_git_log_for_file(&req.dir, req.rela_path).await?) } async fn cmd_git_file_diff_for_commit( _ctx: ClientCtx, req: CmdGitFileDiffForCommitReq, ) -> Result { Ok(crate::git_ext::cmd_git_file_diff_for_commit(&req.dir, &req.commit_oid, req.rela_path) .await?) } async fn cmd_git_initialize( _ctx: ClientCtx, req: CmdGitInitializeReq, ) -> Result<()> { Ok(crate::git_ext::cmd_git_initialize(&req.dir).await?) } async fn cmd_git_clone(_ctx: ClientCtx, req: CmdGitCloneReq) -> Result { Ok(crate::git_ext::cmd_git_clone(&req.url, &req.dir).await?) } async fn cmd_git_commit(_ctx: ClientCtx, req: CmdGitCommitReq) -> Result<()> { Ok(crate::git_ext::cmd_git_commit(&req.dir, &req.message).await?) } async fn cmd_git_fetch_all(_ctx: ClientCtx, req: CmdGitFetchAllReq) -> Result<()> { Ok(crate::git_ext::cmd_git_fetch_all(&req.dir).await?) } async fn cmd_git_push(_ctx: ClientCtx, req: CmdGitPushReq) -> Result { Ok(crate::git_ext::cmd_git_push(&req.dir).await?) } async fn cmd_git_pull(_ctx: ClientCtx, req: CmdGitPullReq) -> Result { Ok(crate::git_ext::cmd_git_pull(&req.dir).await?) } async fn cmd_git_pull_force_reset( _ctx: ClientCtx, req: CmdGitPullForceResetReq, ) -> Result { Ok(crate::git_ext::cmd_git_pull_force_reset(&req.dir, &req.remote, &req.branch).await?) } async fn cmd_git_pull_merge( _ctx: ClientCtx, req: CmdGitPullMergeReq, ) -> Result { Ok(crate::git_ext::cmd_git_pull_merge(&req.dir, &req.remote, &req.branch).await?) } async fn cmd_git_add(_ctx: ClientCtx, req: CmdGitAddReq) -> Result<()> { Ok(crate::git_ext::cmd_git_add(&req.dir, req.rela_paths).await?) } async fn cmd_git_unstage(_ctx: ClientCtx, req: CmdGitUnstageReq) -> Result<()> { Ok(crate::git_ext::cmd_git_unstage(&req.dir, req.rela_paths).await?) } async fn cmd_git_reset_changes( _ctx: ClientCtx, req: CmdGitResetChangesReq, ) -> Result<()> { Ok(crate::git_ext::cmd_git_reset_changes(&req.dir).await?) } async fn cmd_git_restore_files( _ctx: ClientCtx, req: CmdGitRestoreFilesReq, ) -> Result<()> { Ok(crate::git_ext::cmd_git_restore_files(&req.dir, req.rela_paths).await?) } async fn cmd_git_restore_file_from_commit( _ctx: ClientCtx, req: CmdGitRestoreFileFromCommitReq, ) -> Result<()> { Ok(crate::git_ext::cmd_git_restore_file_from_commit(&req.dir, &req.commit_oid, req.rela_path) .await?) } async fn cmd_git_add_credential( _ctx: ClientCtx, req: CmdGitAddCredentialReq, ) -> Result<()> { Ok(crate::git_ext::cmd_git_add_credential(&req.remote_url, &req.username, &req.password) .await?) } async fn cmd_git_remotes( _ctx: ClientCtx, req: CmdGitRemotesReq, ) -> Result> { Ok(crate::git_ext::cmd_git_remotes(&req.dir).await?) } async fn cmd_git_add_remote( _ctx: ClientCtx, req: CmdGitAddRemoteReq, ) -> Result { Ok(crate::git_ext::cmd_git_add_remote(&req.dir, &req.name, &req.url).await?) } async fn cmd_git_rm_remote(_ctx: ClientCtx, req: CmdGitRmRemoteReq) -> Result<()> { Ok(crate::git_ext::cmd_git_rm_remote(&req.dir, &req.name).await?) } async fn cmd_sync_calculate( ctx: ClientCtx, req: CmdSyncCalculateReq, ) -> Result> { Ok(crate::sync_ext::cmd_sync_calculate( ctx.window.app_handle().clone(), &req.workspace_id, &req.sync_dir, ) .await?) } async fn cmd_sync_calculate_fs( _ctx: ClientCtx, req: CmdSyncCalculateFsReq, ) -> Result> { Ok(crate::sync_ext::cmd_sync_calculate_fs(&req.dir).await?) } async fn cmd_sync_apply(ctx: ClientCtx, req: CmdSyncApplyReq) -> Result<()> { Ok(crate::sync_ext::cmd_sync_apply( ctx.window.app_handle().clone(), req.sync_ops, &req.sync_dir, &req.workspace_id, ) .await?) } async fn cmd_ws_delete_connections( ctx: ClientCtx, req: CmdWsDeleteConnectionsReq, ) -> Result<()> { Ok(yaak_commands::models::cmd_ws_delete_connections(ctx, req).await?) } async fn cmd_ws_send( ctx: ClientCtx, req: CmdWsSendReq, ) -> Result { Ok(crate::ws_ext::cmd_ws_send( &req.connection_id, req.environment_id.as_deref(), ctx.window.app_handle().clone(), ctx.window.clone(), ctx.window.app_handle().state::>(), ) .await?) } async fn cmd_ws_close( ctx: ClientCtx, req: CmdWsCloseReq, ) -> Result { Ok(crate::ws_ext::cmd_ws_close( &req.connection_id, ctx.window.app_handle().clone(), ctx.window.clone(), ctx.window.app_handle().state::>(), ) .await?) } async fn cmd_ws_connect( ctx: ClientCtx, req: CmdWsConnectReq, ) -> Result { Ok(crate::ws_ext::cmd_ws_connect( &req.request_id, req.environment_id.as_deref(), req.cookie_jar_id.as_deref(), ctx.window.app_handle().clone(), ctx.window.clone(), ctx.window.app_handle().state::>(), ) .await?) } async fn cmd_plugins_search( ctx: ClientCtx, req: CmdPluginsSearchReq, ) -> Result { Ok(crate::plugins_ext::cmd_plugins_search(ctx.window.app_handle().clone(), &req.query).await?) } async fn cmd_plugins_install( ctx: ClientCtx, req: CmdPluginsInstallReq, ) -> Result<()> { Ok(crate::plugins_ext::cmd_plugins_install(ctx.window.clone(), &req.name, req.version).await?) } async fn cmd_plugins_install_from_directory( ctx: ClientCtx, req: CmdPluginsInstallFromDirectoryReq, ) -> Result { Ok(crate::plugins_ext::cmd_plugins_install_from_directory(ctx.window.clone(), &req.directory) .await?) } async fn cmd_plugins_uninstall( ctx: ClientCtx, req: CmdPluginsUninstallReq, ) -> Result { Ok(crate::plugins_ext::cmd_plugins_uninstall(&req.plugin_id, ctx.window.clone()).await?) } async fn cmd_plugin_init_errors( ctx: ClientCtx, req: CmdPluginInitErrorsReq, ) -> Result> { Ok(yaak_commands::plugins::cmd_plugin_init_errors(ctx, req).await?) } async fn cmd_plugins_updates( ctx: ClientCtx, _req: CmdPluginsUpdatesReq, ) -> Result { Ok(crate::plugins_ext::cmd_plugins_updates(ctx.window.app_handle().clone()).await?) } async fn cmd_plugins_update_all( ctx: ClientCtx, _req: CmdPluginsUpdateAllReq, ) -> Result> { Ok(crate::plugins_ext::cmd_plugins_update_all(ctx.window.clone()).await?) }