Route all app commands through a single RPC envelope (#542)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-08-14 14:16:14 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 1f91cddab9
commit 23e7229e63
37 changed files with 2473 additions and 395 deletions
Generated
+2
View File
@@ -11034,6 +11034,7 @@ dependencies = [
"yaak-mac-window",
"yaak-models",
"yaak-plugins",
"yaak-rpc",
"yaak-sse",
"yaak-sync",
"yaak-system-appearance",
@@ -11201,6 +11202,7 @@ dependencies = [
"tokio-stream",
"tonic",
"tonic-reflection",
"ts-rs",
"uuid",
"yaak-common",
"yaak-tls",
@@ -1,10 +1,9 @@
import type { LicenseCheckStatus } from "@yaakapp-internal/license";
import { checkLicense } from "@yaakapp-internal/license";
import { useCallback, useEffect, useRef, useState } from "react";
import { useKeyValue } from "../hooks/useKeyValue";
import { appInfo } from "../lib/appInfo";
import { pricingUrl } from "../lib/pricingUrl";
import { DismissibleBanner } from "./core/DismissibleBanner";
import { rpc } from "../lib/rpc";
import { platform } from "@yaakapp-internal/platform";
const COMMERCIAL_USE_SNOOZE_MS = 7 * 24 * 60 * 60 * 1000;
@@ -95,7 +94,7 @@ async function shouldShowCommercialUsePrompt(): Promise<boolean> {
}
try {
const license = await rpc<LicenseCheckStatus>("plugin:yaak-license|check");
const license = await checkLicense();
return license.status === "personal_use";
} catch (err) {
console.log("Failed to check license before commercial-use prompt", err);
+8 -64
View File
@@ -1,73 +1,17 @@
import type { RpcPayload } from "@yaakapp-internal/platform";
import { platform } from "@yaakapp-internal/platform";
import type { RpcSchema } from "@yaakapp-internal/tauri-client";
/**
* Every backend command the client sends.
* Every backend command the app can call: the generated wire schema, one field
* per `RpcRouter` registration on the Rust side. A typo'd or unregistered
* command name is a compile error. Host plugin commands (license, fonts,
* mac-window) don't appear here — each lives behind its own package's facade.
*
* Listing them keeps typos out and gives us the inventory to check the Rust
* side against. Once the app's commands move onto `RpcRouter`, this union is
* replaced by the generated `RpcSchema` and the payload and result types come
* with it, the way `apps/yaak-proxy/lib/rpc.ts` already works.
* `RpcSchema` also carries each command's request and response payload types;
* adopting them at call sites is an incremental follow-up.
*/
type AppCmd =
| "cmd_call_grpc_request_action"
| "cmd_call_http_authentication_action"
| "cmd_call_http_request_action"
| "cmd_call_websocket_request_action"
| "cmd_call_workspace_action"
| "cmd_call_folder_action"
| "cmd_check_for_updates"
| "cmd_curl_to_request"
| "cmd_decrypt_template"
| "cmd_default_headers"
| "cmd_delete_all_grpc_connections"
| "cmd_delete_all_http_responses"
| "cmd_delete_send_history"
| "cmd_dismiss_notification"
| "cmd_export_data"
| "cmd_format_graphql"
| "cmd_format_json"
| "cmd_get_http_authentication_config"
| "cmd_get_http_authentication_summaries"
| "cmd_get_http_response_events"
| "cmd_get_sse_events"
| "cmd_get_themes"
| "cmd_get_workspace_meta"
| "cmd_git_add_credential"
| "cmd_git_clone"
| "cmd_grpc_go"
| "cmd_grpc_reflect"
| "cmd_grpc_request_actions"
| "cmd_http_request_actions"
| "cmd_websocket_request_actions"
| "cmd_workspace_actions"
| "cmd_folder_actions"
| "cmd_http_request_body"
| "cmd_http_response_body"
| "cmd_import_data"
| "cmd_metadata"
| "cmd_restart"
| "cmd_new_child_window"
| "cmd_new_main_window"
| "cmd_plugin_info"
| "cmd_plugin_init_errors"
| "cmd_reload_plugins"
| "cmd_render_template"
| "cmd_save_base64_to_binary"
| "cmd_save_response"
| "cmd_secure_template"
| "cmd_send_ephemeral_request"
| "cmd_send_feedback"
| "cmd_send_http_request"
| "cmd_template_function_summaries"
| "cmd_template_function_config"
| "cmd_template_tokens_to_string"
| "models_get_graphql_introspection"
| "models_get_settings"
| "models_grpc_events"
| "models_upsert_graphql_introspection"
| "models_websocket_events"
| "plugin:yaak-license|check";
type AppCmd = keyof RpcSchema;
/** Call a backend command. */
export function rpc<T>(cmd: AppCmd, payload?: RpcPayload): Promise<T> {
+5 -3
View File
@@ -9,7 +9,7 @@ use log::warn;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Mutex;
use std::sync::{Arc, Mutex};
use ts_rs::TS;
use yaak_database::{ModelChangeEvent, UpdateSource};
use yaak_proxy::{CapturedRequest, ProxyEvent, ProxyHandle, RequestState};
@@ -17,15 +17,17 @@ use yaak_rpc::{RpcError, RpcEventEmitter, define_rpc};
// -- Context --
// Cloned once per dispatched command, so shared state lives behind an `Arc`.
#[derive(Clone)]
pub struct ProxyCtx {
handle: Mutex<Option<ProxyHandle>>,
handle: Arc<Mutex<Option<ProxyHandle>>>,
pub db: ProxyQueryManager,
pub events: RpcEventEmitter,
}
impl ProxyCtx {
pub fn new(db_path: &Path, events: RpcEventEmitter) -> Self {
Self { handle: Mutex::new(None), db: ProxyQueryManager::new(db_path), events }
Self { handle: Arc::new(Mutex::new(None)), db: ProxyQueryManager::new(db_path), events }
}
}
+1
View File
@@ -72,6 +72,7 @@ tokio-tungstenite = { version = "0.26.2", default-features = false }
url = "2"
tokio-util = { version = "0.7", features = ["codec"] }
ts-rs = { workspace = true }
yaak-rpc = { workspace = true }
uuid = "1.12.1"
yaak-api = { workspace = true }
yaak-common = { workspace = true }
+8
View File
@@ -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 { PluginVersion } from "./gen_search";
export type PluginNameVersion = { name: string, version: string, };
export type PluginSearchResponse = { plugins: Array<PluginVersion>, };
export type PluginUpdatesResponse = { plugins: Array<PluginNameVersion>, };
+389
View File
@@ -0,0 +1,389 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from "./gen_models";
export type CallFolderActionArgs = { folder: Folder, };
export type CallFolderActionRequest = { index: number, pluginRefId: string, args: CallFolderActionArgs, };
export type CallGrpcRequestActionArgs = { grpcRequest: GrpcRequest, protoFiles: Array<string>, };
export type CallGrpcRequestActionRequest = { index: number, pluginRefId: string, args: CallGrpcRequestActionArgs, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallWebsocketRequestActionArgs = { websocketRequest: WebsocketRequest, };
export type CallWebsocketRequestActionRequest = { index: number, pluginRefId: string, args: CallWebsocketRequestActionArgs, };
export type CallWorkspaceActionArgs = { workspace: Workspace, };
export type CallWorkspaceActionRequest = { index: number, pluginRefId: string, args: CallWorkspaceActionArgs, };
export type Color = "primary" | "secondary" | "info" | "success" | "notice" | "warning" | "danger";
export type CompletionOptionType = "constant" | "variable";
export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown" | "c" | "clojure" | "csharp" | "go" | "http" | "java" | "kotlin" | "objective_c" | "ocaml" | "php" | "powershell" | "python" | "r" | "ruby" | "shell" | "swift";
export type FileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
export type FilterResponse = { content: string, error?: string, };
export type FolderAction = { label: string, icon?: Icon, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "h_stack" } & FormInputHStack | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown | { "type": "key_value" } & FormInputKeyValue;
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
export type FormInputBanner = { inputs?: Array<FormInput>, hidden?: boolean, color?: Color, };
export type FormInputCheckbox = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputEditor = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Don't show the editor gutter (line numbers, folds, etc.)
*/
hideGutter?: boolean,
/**
* Language for syntax highlighting
*/
language?: EditorLanguage, readOnly?: boolean,
/**
* Fixed number of visible rows
*/
rows?: number, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputFile = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputHStack = { inputs?: Array<FormInput>, hidden?: boolean, };
export type FormInputHttpRequest = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputKeyValue = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputMarkdown = { content: string, hidden?: boolean, };
export type FormInputSelect = {
/**
* The options that will be available in the select input
*/
options: Array<FormInputSelectOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputSelectOption = { label: string, value: string, };
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Placeholder for the text input
*/
password?: boolean,
/**
* Whether to allow newlines in the input, like a <textarea/>
*/
multiLine?: boolean, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type GenericCompletionOption = { label: string, detail?: string, info?: string, type?: CompletionOptionType, boost?: number, };
export type GetFolderActionsResponse = { actions: Array<FolderAction>, pluginRefId: string, };
export type GetGrpcRequestActionsResponse = { actions: Array<GrpcRequestAction>, pluginRefId: string, };
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
export type GetHttpAuthenticationSummaryResponse = { name: string, label: string, shortLabel: string, };
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetTemplateFunctionConfigResponse = { function: TemplateFunction, pluginRefId: string, };
export type GetTemplateFunctionSummaryResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type GetThemesResponse = { themes: Array<Theme>, };
export type GetWebsocketRequestActionsResponse = { actions: Array<WebsocketRequestAction>, pluginRefId: string, };
export type GetWorkspaceActionsResponse = { actions: Array<WorkspaceAction>, pluginRefId: string, };
export type GrpcRequestAction = { label: string, icon?: Icon, };
export type HttpAuthenticationAction = { label: string, icon?: Icon, };
export type HttpRequestAction = { label: string, icon?: Icon, };
export type Icon = "alert_triangle" | "check" | "check_circle" | "chevron_down" | "copy" | "info" | "pin" | "search" | "trash" | "_unknown";
export type JsonPrimitive = string | number | boolean | null;
export type RenderPurpose = "send" | "preview";
export type TemplateFunction = { name: string, previewType?: TemplateFunctionPreviewType, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<TemplateFunctionArg>,
/**
* A list of arg names to show in the inline preview. If not provided, none will be shown (for privacy reasons).
*/
previewArgs?: Array<string>, };
/**
* Similar to FormInput, but contains
*/
export type TemplateFunctionArg = FormInput;
export type TemplateFunctionPreviewType = "live" | "click" | "none";
export type Theme = {
/**
* How the theme is identified. This should never be changed
*/
id: string,
/**
* The friendly name of the theme to be displayed to the user
*/
label: string,
/**
* Whether the theme will be used for dark or light appearance
*/
dark: boolean,
/**
* The default top-level colors for the theme
*/
base: ThemeComponentColors,
/**
* Optionally override theme for individual UI components for more control
*/
components?: ThemeComponents, };
export type ThemeComponentColors = { surface?: string, surfaceHighlight?: string, surfaceActive?: string, text?: string, textSubtle?: string, textSubtlest?: string, border?: string, borderSubtle?: string, borderFocus?: string, shadow?: string, backdrop?: string, selection?: string, primary?: string, secondary?: string, info?: string, success?: string, notice?: string, warning?: string, danger?: string, };
export type ThemeComponents = { dialog?: ThemeComponentColors, menu?: ThemeComponentColors, toast?: ThemeComponentColors, sidebar?: ThemeComponentColors, responsePane?: ThemeComponentColors, appHeader?: ThemeComponentColors, button?: ThemeComponentColors, banner?: ThemeComponentColors, templateTag?: ThemeComponentColors, urlBar?: ThemeComponentColors, editor?: ThemeComponentColors, input?: ThemeComponentColors, };
export type WebsocketRequestAction = { label: string, icon?: Icon, };
export type WorkspaceAction = { label: string, icon?: Icon, };
+35
View File
@@ -0,0 +1,35 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SyncModel } from "./gen_models";
export type BranchDeleteResult = { "type": "success", message: string, } | { "type": "not_fully_merged" };
export type CloneResult = { "type": "success" } | { "type": "cancelled" } | { "type": "needs_credentials", url: string, error: string | null, };
export type GitAuthor = { name: string | null, email: string | null, };
export type GitBranchInfo = { path: string, headRef: string | null, headRefShorthand: string | null, origins: Array<string>, localBranches: Array<string>, remoteBranches: Array<string>, ahead: number, behind: number, };
export type GitCommit = { oid: string, author: GitAuthor, when: string, message: string | null, };
export type GitFileDiff = { original: string, modified: string, };
export type GitRemote = { name: string, url: string | null, };
export type GitStatus = "untracked" | "conflict" | "current" | "modified" | "removed" | "renamed" | "type_change";
export type GitStatusEntry = { relaPath: string, status: GitStatus, staged: boolean, prev: SyncModel | null, next: SyncModel | null, };
export type GitStatusSummary = { path: string,
/**
* The status directory relative to the repo root ("" when it IS the root).
* Useful for displaying entry paths relative to the sync directory
*/
relaDir: string, headRef: string | null, headRefShorthand: string | null, entries: Array<GitStatusEntry>, origins: Array<string>, localBranches: Array<string>, remoteBranches: Array<string>, ahead: number, behind: number, };
export type GitWorktreeStatus = { entries: Array<GitWorktreeStatusEntry>, };
export type GitWorktreeStatusEntry = { relaPath: string, modelId: string | null, status: GitStatus, staged: boolean, };
export type PullResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, } | { "type": "diverged", remote: string, branch: string, } | { "type": "uncommitted_changes" };
export type PushResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, };
+5
View File
@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type MethodDefinition = { name: string, schema: string, clientStreaming: boolean, serverStreaming: boolean, };
export type ServiceDefinition = { name: string, methods: Array<MethodDefinition>, };
+116
View File
@@ -0,0 +1,116 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AnyModel = CookieJar | Environment | Folder | GraphQlIntrospection | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | HttpResponseEvent | KeyValue | Plugin | Settings | SyncState | WebsocketConnection | WebsocketEvent | WebsocketRequest | Workspace | WorkspaceMeta;
export type ClientCertificate = { host: string, port: number | null, crtFile: string | null, keyFile: string | null, pfxFile: string | null, passphrase: string | null, enabled?: boolean, };
export type Cookie = { name: string, value: string, domain: CookieDomain, expires: CookieExpires, path: string, secure: boolean, httpOnly: boolean, sameSite: CookieSameSite | null, };
export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty";
export type CookieExpires = { "AtUtc": string } | "SessionEnd";
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
export type CookieSameSite = "Strict" | "Lax" | "None";
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
export type EncryptedKey = { encryptedKey: string, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
/**
* Variables defined in this environment scope.
* Child environments override parent variables by name.
*/
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, settingSendCookies: InheritedBoolSetting, settingStoreCookies: InheritedBoolSetting, settingValidateCertificates: InheritedBoolSetting, settingFollowRedirects: InheritedBoolSetting, settingRequestTimeout: InheritedIntSetting, settingRequestMessageSize: InheritedIntSetting, };
export type GraphQlIntrospection = { model: "graphql_introspection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, content: string | null, };
export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, };
export type GrpcConnectionState = "initialized" | "connected" | "closed";
export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, content: string, error: string | null, eventType: GrpcEventType, metadata: { [key in string]?: string }, status: number | null, };
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number,
/**
* Server URL (http for plaintext or https for secure)
*/
url: string, settingValidateCertificates: InheritedBoolSetting, settingRequestMessageSize: InheritedIntSetting, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
/**
* URL parameters used for both path placeholders (`:id`) and query string entries.
*/
urlParameters: Array<HttpUrlParameter>, settingSendCookies: InheritedBoolSetting, settingStoreCookies: InheritedBoolSetting, settingValidateCertificates: InheritedBoolSetting, settingFollowRedirects: InheritedBoolSetting, settingRequestTimeout: InheritedIntSetting, };
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
/**
* Serializable representation of HTTP response events for DB storage.
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
* The `From` impl is in yaak-http to avoid circular dependencies.
*/
export type HttpResponseEventData = { "type": "setting", name: string, value: string, source_model?: string, source_id?: string, source_name?: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
export type HttpResponseHeader = { name: string, value: string, };
export type HttpResponseState = "initialized" | "connected" | "closed";
export type HttpUrlParameter = { enabled?: boolean,
/**
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
* Other entries are appended as query parameters
*/
name: string, value: string, id?: string, };
export type InheritedBoolSetting = { enabled?: boolean, value: boolean, };
export type InheritedIntSetting = { enabled?: boolean, value: number, };
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
export type PluginSource = "bundled" | "filesystem" | "registry";
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
export type ProxySettingAuth = { user: string, password: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, clientCertificates: Array<ClientCertificate>, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, useNativeTitlebar: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, promptFeedback: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, checkNotifications: boolean, hotkeys: { [key in string]?: Array<string> }, };
export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environment" } & Environment | { "type": "folder" } & Folder | { "type": "http_request" } & HttpRequest | { "type": "grpc_request" } & GrpcRequest | { "type": "websocket_request" } & WebsocketRequest;
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
export type WebsocketConnection = { model: "websocket_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, headers: Array<HttpResponseHeader>, state: WebsocketConnectionState, status: number, url: string, };
export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed";
export type WebsocketEvent = { model: "websocket_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, isServer: boolean, message: Array<number>, messageType: WebsocketEventType, };
export type WebsocketEventType = "binary" | "close" | "error" | "frame" | "open" | "ping" | "pong" | "text";
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string,
/**
* URL parameters used for both path placeholders (`:id`) and query string entries.
*/
urlParameters: Array<HttpUrlParameter>, settingSendCookies: InheritedBoolSetting, settingStoreCookies: InheritedBoolSetting, settingValidateCertificates: InheritedBoolSetting, settingRequestMessageSize: InheritedIntSetting, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingRequestMessageSize: number, settingDnsOverrides: Array<DnsOverride>, settingSendCookies: boolean, settingStoreCookies: boolean, };
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
File diff suppressed because one or more lines are too long
+5
View File
@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PluginMetadata = { version: string, name: string, displayName: string, description: string | null, homepageUrl: string | null, repositoryUrl: string | null, };
export type PluginVersion = { id: string, version: string, url: string, description: string | null, name: string, displayName: string, homepageUrl: string | null, repositoryUrl: string | null, checksum: string, readme: string | null, yanked: boolean, };
+6
View File
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SyncModel, SyncState } from "./gen_models";
export type FsCandidate = { "type": "FsCandidate", model: SyncModel, relPath: string, checksum: string, };
export type SyncOp = { "type": "fsCreate", model: SyncModel, } | { "type": "fsUpdate", model: SyncModel, state: SyncState, } | { "type": "fsDelete", state: SyncState, fs: FsCandidate | null, } | { "type": "dbCreate", fs: FsCandidate, } | { "type": "dbUpdate", state: SyncState, fs: FsCandidate, } | { "type": "dbDelete", model: SyncModel, state: SyncState, } | { "type": "ignorePrivate", model: SyncModel, };
+4
View File
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from "./gen_models";
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };
+2 -2
View File
@@ -1,5 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type GitWatchResult = { unlistenEvent: string, };
export type PluginUpdateInfo = { name: string, currentVersion: string, latestVersion: string, };
export type PluginUpdateNotification = { updateCount: number, plugins: Array<PluginUpdateInfo>, };
@@ -12,8 +14,6 @@ export type UpdateResponseAction = "install" | "skip";
export type WatchResult = { unlistenEvent: string, };
export type GitWatchResult = { unlistenEvent: string, };
export type YaakNotification = { timestamp: string, timeout: number | null, id: string, title: string | null, message: string, color: string | null, action: YaakNotificationAction | null, };
export type YaakNotificationAction = { label: string, url: string, };
+9
View File
@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type FnArg = { name: string, value: Val, };
export type Token = { "type": "raw", text: string, } | { "type": "tag", val: Val, } | { "type": "eof" };
export type Tokens = { tokens: Array<Token>, };
export type Val = { "type": "str", text: string, } | { "type": "var", name: string, } | { "type": "bool", value: boolean, } | { "type": "fn", name: string, args: Array<FnArg>, } | { "type": "null" };
+3
View File
@@ -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 ServerSentEvent = { eventType: string, data: string, id: string | null, retry: bigint | null, };
+4
View File
@@ -0,0 +1,4 @@
// ts-rs owns bindings/index.ts and rewrites it on export, so this hand-written
// entry point is where the generated files come together.
export * from "./bindings/gen_rpc";
export * from "./bindings/index";
+1 -1
View File
@@ -2,5 +2,5 @@
"name": "@yaakapp-internal/tauri-client",
"version": "1.0.0",
"private": true,
"main": "bindings/index.ts"
"main": "index.ts"
}
+1 -9
View File
@@ -1,7 +1,7 @@
use crate::PluginContextExt;
use crate::error::Result;
use std::sync::Arc;
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow, command};
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow};
use yaak_crypto::manager::EncryptionManager;
use yaak_models::models::HttpRequestHeader;
use yaak_models::queries::workspaces::default_headers;
@@ -22,7 +22,6 @@ impl<'a, R: Runtime, M: Manager<R>> EncryptionManagerExt<'a, R> for M {
}
}
#[command]
pub(crate) async fn cmd_decrypt_template<R: Runtime>(
window: WebviewWindow<R>,
template: &str,
@@ -32,7 +31,6 @@ pub(crate) async fn cmd_decrypt_template<R: Runtime>(
Ok(decrypt_secure_template_function(&encryption_manager, &plugin_context, template)?)
}
#[command]
pub(crate) async fn cmd_secure_template<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
@@ -49,7 +47,6 @@ pub(crate) async fn cmd_secure_template<R: Runtime>(
)?)
}
#[command]
pub(crate) async fn cmd_get_themes<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
@@ -57,7 +54,6 @@ pub(crate) async fn cmd_get_themes<R: Runtime>(
Ok(plugin_manager.get_themes(&window.plugin_context()).await?)
}
#[command]
pub(crate) async fn cmd_enable_encryption<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: &str,
@@ -67,7 +63,6 @@ pub(crate) async fn cmd_enable_encryption<R: Runtime>(
Ok(())
}
#[command]
pub(crate) async fn cmd_reveal_workspace_key<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: &str,
@@ -75,7 +70,6 @@ pub(crate) async fn cmd_reveal_workspace_key<R: Runtime>(
Ok(window.crypto().reveal_workspace_key(workspace_id)?)
}
#[command]
pub(crate) async fn cmd_set_workspace_key<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: &str,
@@ -85,7 +79,6 @@ pub(crate) async fn cmd_set_workspace_key<R: Runtime>(
Ok(())
}
#[command]
pub(crate) async fn cmd_disable_encryption<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: &str,
@@ -94,7 +87,6 @@ pub(crate) async fn cmd_disable_encryption<R: Runtime>(
Ok(())
}
#[command]
pub(crate) fn cmd_default_headers() -> Vec<HttpRequestHeader> {
default_headers()
}
@@ -3,10 +3,7 @@
//! This module provides the Tauri commands for git functionality.
use crate::error::Result;
use crate::git_watcher::{GitWatchResult, watch_git_worktree_status};
use std::path::{Path, PathBuf};
use tauri::ipc::Channel;
use tauri::{AppHandle, Runtime, command};
use yaak_git::{
BranchDeleteResult, CloneResult, GitBranchInfo, GitCommit, GitFileDiff, GitRemote,
GitStatusSummary, GitWorktreeStatus, PullResult, PushResult, git_add, git_add_credential,
@@ -19,17 +16,14 @@ use yaak_git::{
// NOTE: All of these commands are async to prevent blocking work from locking up the UI
#[command]
pub async fn cmd_git_checkout(dir: &Path, branch: &str, force: bool) -> Result<String> {
Ok(git_checkout_branch(dir, branch, force).await?)
}
#[command]
pub async fn cmd_git_branch(dir: &Path, branch: &str, base: Option<&str>) -> Result<()> {
Ok(git_create_branch(dir, branch, base).await?)
}
#[command]
pub async fn cmd_git_delete_branch(
dir: &Path,
branch: &str,
@@ -38,56 +32,38 @@ pub async fn cmd_git_delete_branch(
Ok(git_delete_branch(dir, branch, force.unwrap_or(false)).await?)
}
#[command]
pub async fn cmd_git_delete_remote_branch(dir: &Path, branch: &str) -> Result<()> {
Ok(git_delete_remote_branch(dir, branch).await?)
}
#[command]
pub async fn cmd_git_merge_branch(dir: &Path, branch: &str) -> Result<()> {
Ok(git_merge_branch(dir, branch).await?)
}
#[command]
pub async fn cmd_git_rename_branch(dir: &Path, old_name: &str, new_name: &str) -> Result<()> {
Ok(git_rename_branch(dir, old_name, new_name).await?)
}
#[command]
pub async fn cmd_git_status(dir: &Path) -> Result<GitStatusSummary> {
Ok(git_status(dir)?)
}
#[command]
pub async fn cmd_git_branch_info(dir: &Path) -> Result<GitBranchInfo> {
Ok(git_branch_info(dir)?)
}
#[command]
pub async fn cmd_git_worktree_status(dir: &Path) -> Result<GitWorktreeStatus> {
Ok(git_worktree_status(dir)?)
}
#[command]
pub async fn cmd_git_watch_worktree_status<R: Runtime>(
app_handle: AppHandle<R>,
dir: &Path,
channel: Channel<GitWorktreeStatus>,
) -> Result<GitWatchResult> {
watch_git_worktree_status(app_handle, dir, channel).await
}
#[command]
pub async fn cmd_git_log(dir: &Path) -> Result<Vec<GitCommit>> {
Ok(git_log(dir)?)
}
#[command]
pub async fn cmd_git_log_for_file(dir: &Path, rela_path: PathBuf) -> Result<Vec<GitCommit>> {
Ok(git_log_for_file(dir, &rela_path)?)
}
#[command]
pub async fn cmd_git_file_diff_for_commit(
dir: &Path,
commit_oid: &str,
@@ -96,37 +72,30 @@ pub async fn cmd_git_file_diff_for_commit(
Ok(git_file_diff_for_commit(dir, commit_oid, &rela_path)?)
}
#[command]
pub async fn cmd_git_initialize(dir: &Path) -> Result<()> {
Ok(git_init(dir)?)
}
#[command]
pub async fn cmd_git_clone(url: &str, dir: &Path) -> Result<CloneResult> {
Ok(git_clone(url, dir).await?)
}
#[command]
pub async fn cmd_git_commit(dir: &Path, message: &str) -> Result<()> {
Ok(git_commit(dir, message).await?)
}
#[command]
pub async fn cmd_git_fetch_all(dir: &Path) -> Result<()> {
Ok(git_fetch_all(dir).await?)
}
#[command]
pub async fn cmd_git_push(dir: &Path) -> Result<PushResult> {
Ok(git_push(dir).await?)
}
#[command]
pub async fn cmd_git_pull(dir: &Path) -> Result<PullResult> {
Ok(git_pull(dir).await?)
}
#[command]
pub async fn cmd_git_pull_force_reset(
dir: &Path,
remote: &str,
@@ -135,12 +104,10 @@ pub async fn cmd_git_pull_force_reset(
Ok(git_pull_force_reset(dir, remote, branch).await?)
}
#[command]
pub async fn cmd_git_pull_merge(dir: &Path, remote: &str, branch: &str) -> Result<PullResult> {
Ok(git_pull_merge(dir, remote, branch).await?)
}
#[command]
pub async fn cmd_git_add(dir: &Path, rela_paths: Vec<PathBuf>) -> Result<()> {
for path in rela_paths {
git_add(dir, &path)?;
@@ -148,7 +115,6 @@ pub async fn cmd_git_add(dir: &Path, rela_paths: Vec<PathBuf>) -> Result<()> {
Ok(())
}
#[command]
pub async fn cmd_git_unstage(dir: &Path, rela_paths: Vec<PathBuf>) -> Result<()> {
for path in rela_paths {
git_unstage(dir, &path)?;
@@ -156,12 +122,10 @@ pub async fn cmd_git_unstage(dir: &Path, rela_paths: Vec<PathBuf>) -> Result<()>
Ok(())
}
#[command]
pub async fn cmd_git_reset_changes(dir: &Path) -> Result<()> {
Ok(git_reset_changes(dir).await?)
}
#[command]
pub async fn cmd_git_restore_files(dir: &Path, rela_paths: Vec<PathBuf>) -> Result<()> {
for path in rela_paths {
git_restore(dir, &path)?;
@@ -169,7 +133,6 @@ pub async fn cmd_git_restore_files(dir: &Path, rela_paths: Vec<PathBuf>) -> Resu
Ok(())
}
#[command]
pub async fn cmd_git_restore_file_from_commit(
dir: &Path,
commit_oid: &str,
@@ -178,7 +141,6 @@ pub async fn cmd_git_restore_file_from_commit(
Ok(git_restore_file_from_commit(dir, commit_oid, &rela_path)?)
}
#[command]
pub async fn cmd_git_add_credential(
remote_url: &str,
username: &str,
@@ -187,17 +149,14 @@ pub async fn cmd_git_add_credential(
Ok(git_add_credential(remote_url, username, password).await?)
}
#[command]
pub async fn cmd_git_remotes(dir: &Path) -> Result<Vec<GitRemote>> {
Ok(git_remotes(dir)?)
}
#[command]
pub async fn cmd_git_add_remote(dir: &Path, name: &str, url: &str) -> Result<GitRemote> {
Ok(git_add_remote(dir, name, url)?)
}
#[command]
pub async fn cmd_git_rm_remote(dir: &Path, name: &str) -> Result<()> {
Ok(git_rm_remote(dir, name)?)
}
+14 -15
View File
@@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::mpsc;
use std::time::Duration;
use tauri::ipc::Channel;
use tauri::{AppHandle, Listener, Runtime};
use tokio::select;
use tokio::sync::watch;
@@ -23,11 +22,15 @@ pub(crate) struct GitWatchResult {
unlisten_event: String,
}
pub(crate) async fn watch_git_worktree_status<R: Runtime>(
pub(crate) async fn watch_git_worktree_status<R, F>(
app_handle: AppHandle<R>,
dir: &Path,
channel: Channel<GitWorktreeStatus>,
) -> Result<GitWatchResult> {
on_status: F,
) -> Result<GitWatchResult>
where
R: Runtime,
F: Fn(GitWorktreeStatus) + Send + Sync + 'static,
{
let paths = git_repository_paths(dir)?;
let repo_dir = dir.to_path_buf();
let workdir = paths.workdir;
@@ -76,7 +79,7 @@ pub(crate) async fn watch_git_worktree_status<R: Runtime>(
let (cancel_tx, cancel_rx) = watch::channel(());
let mut cancel_rx = cancel_rx;
send_worktree_status(&repo_dir, &channel);
send_worktree_status(&repo_dir, &on_status);
tauri::async_runtime::spawn(async move {
let _watcher = watcher;
@@ -90,7 +93,7 @@ pub(crate) async fn watch_git_worktree_status<R: Runtime>(
&workdir,
&gitdir,
&commondir,
&channel,
&on_status,
).await;
}
_ = cancel_rx.changed() => {
@@ -119,13 +122,13 @@ async fn handle_git_watch_event(
workdir: &Path,
gitdir: &Path,
commondir: &Path,
channel: &Channel<GitWorktreeStatus>,
on_status: &impl Fn(GitWorktreeStatus),
) {
if !is_relevant_git_watch_event(event_res, repo_dir, workdir, gitdir, commondir) {
return;
}
send_worktree_status(repo_dir, channel);
send_worktree_status(repo_dir, on_status);
let settle_window = sleep(GIT_STATUS_COALESCE_WINDOW);
tokio::pin!(settle_window);
@@ -140,7 +143,7 @@ async fn handle_git_watch_event(
}
}
send_worktree_status(repo_dir, channel);
send_worktree_status(repo_dir, on_status);
}
fn is_relevant_git_watch_event(
@@ -180,13 +183,9 @@ fn is_relevant_git_watch_event(
false
}
fn send_worktree_status(repo_dir: &Path, channel: &Channel<GitWorktreeStatus>) {
fn send_worktree_status(repo_dir: &Path, on_status: &impl Fn(GitWorktreeStatus)) {
match git_worktree_status(repo_dir) {
Ok(status) => {
if let Err(e) = channel.send(status) {
warn!("Failed to send git worktree status: {:?}", e);
}
}
Ok(status) => on_status(status),
Err(e) => {
warn!("Failed to get git worktree status: {e}");
}
+9 -169
View File
@@ -77,6 +77,7 @@ mod notifications;
mod plugin_events;
mod plugins_ext;
mod render;
mod rpc_ext;
mod sync_ext;
mod updates;
mod uri_scheme;
@@ -181,9 +182,10 @@ impl<R: Runtime> PluginContextExt<R> for WebviewWindow<R> {
}
}
#[derive(serde::Serialize)]
#[derive(serde::Serialize, ts_rs::TS)]
#[serde(default, rename_all = "camelCase")]
struct AppMetaData {
#[ts(export, export_to = "gen_rpc.ts")]
pub struct AppMetaData {
is_dev: bool,
version: String,
cli_version: Option<String>,
@@ -196,7 +198,6 @@ struct AppMetaData {
feature_license: bool,
}
#[tauri::command]
async fn cmd_metadata<R: Runtime>(app_handle: AppHandle<R>) -> YaakResult<AppMetaData> {
let app_data_dir = app_handle.path().app_data_dir()?;
let app_log_dir = app_handle.path().app_log_dir()?;
@@ -236,7 +237,6 @@ async fn detect_cli_version_for_binary(program: &str) -> Option<String> {
Some(parts.next().unwrap_or(line).to_string())
}
#[tauri::command]
async fn cmd_template_tokens_to_string<R: Runtime>(
window: WebviewWindow<R>,
app_handle: AppHandle<R>,
@@ -254,7 +254,6 @@ async fn cmd_template_tokens_to_string<R: Runtime>(
Ok(new_tokens.to_string())
}
#[tauri::command]
async fn cmd_render_template<R: Runtime>(
window: WebviewWindow<R>,
app_handle: AppHandle<R>,
@@ -288,7 +287,6 @@ async fn cmd_render_template<R: Runtime>(
Ok(result)
}
#[tauri::command]
async fn cmd_send_feedback<R: Runtime>(
app_handle: AppHandle<R>,
feature: String,
@@ -298,7 +296,6 @@ async fn cmd_send_feedback<R: Runtime>(
Ok(())
}
#[tauri::command]
async fn cmd_dismiss_notification<R: Runtime>(
window: WebviewWindow<R>,
notification_id: &str,
@@ -307,7 +304,6 @@ async fn cmd_dismiss_notification<R: Runtime>(
Ok(yaak_notifier.lock().await.seen(&window, notification_id).await?)
}
#[tauri::command]
async fn cmd_grpc_reflect<R: Runtime>(
request_id: &str,
environment_id: Option<&str>,
@@ -368,7 +364,6 @@ async fn cmd_grpc_reflect<R: Runtime>(
.map_err(|e| GenericError(e.to_string()))?)
}
#[tauri::command]
async fn cmd_grpc_go<R: Runtime>(
request_id: &str,
environment_id: Option<&str>,
@@ -981,13 +976,11 @@ async fn cmd_grpc_go<R: Runtime>(
Ok(conn.id)
}
#[tauri::command]
async fn cmd_restart<R: Runtime>(app_handle: AppHandle<R>) -> YaakResult<()> {
app_handle.request_restart();
Ok(())
}
#[tauri::command]
async fn cmd_send_ephemeral_request<R: Runtime>(
mut request: HttpRequest,
environment_id: Option<&str>,
@@ -1016,12 +1009,10 @@ async fn cmd_send_ephemeral_request<R: Runtime>(
send_http_request(&window, &request, &response, environment, cookie_jar, &mut cancel_rx).await
}
#[tauri::command]
async fn cmd_format_json(text: &str) -> YaakResult<String> {
Ok(format_json(text, " "))
}
#[tauri::command]
async fn cmd_format_graphql(text: &str) -> YaakResult<String> {
match pretty_graphql::format_text(text, &Default::default()) {
Ok(formatted) => Ok(formatted),
@@ -1029,7 +1020,6 @@ async fn cmd_format_graphql(text: &str) -> YaakResult<String> {
}
}
#[tauri::command]
async fn cmd_http_response_body<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
@@ -1063,7 +1053,6 @@ async fn cmd_http_response_body<R: Runtime>(
}
}
#[tauri::command]
async fn cmd_http_request_body<R: Runtime>(
app_handle: AppHandle<R>,
response_id: &str,
@@ -1080,7 +1069,6 @@ async fn cmd_http_request_body<R: Runtime>(
Ok(Some(body))
}
#[tauri::command]
async fn cmd_get_sse_events(file_path: &str) -> YaakResult<Vec<ServerSentEvent>> {
let body = fs::read(file_path)?;
let mut event_parser = EventParser::new();
@@ -1101,7 +1089,6 @@ async fn cmd_get_sse_events(file_path: &str) -> YaakResult<Vec<ServerSentEvent>>
Ok(events)
}
#[tauri::command]
async fn cmd_get_http_response_events<R: Runtime>(
app_handle: AppHandle<R>,
response_id: &str,
@@ -1110,7 +1097,6 @@ async fn cmd_get_http_response_events<R: Runtime>(
Ok(events)
}
#[tauri::command]
async fn cmd_import_data<R: Runtime>(
window: WebviewWindow<R>,
file_path: &str,
@@ -1118,7 +1104,6 @@ async fn cmd_import_data<R: Runtime>(
import_data(&window, file_path).await
}
#[tauri::command]
async fn cmd_http_request_actions<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
@@ -1126,7 +1111,6 @@ async fn cmd_http_request_actions<R: Runtime>(
Ok(plugin_manager.get_http_request_actions(&window.plugin_context()).await?)
}
#[tauri::command]
async fn cmd_websocket_request_actions<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
@@ -1134,7 +1118,6 @@ async fn cmd_websocket_request_actions<R: Runtime>(
Ok(plugin_manager.get_websocket_request_actions(&window.plugin_context()).await?)
}
#[tauri::command]
async fn cmd_call_websocket_request_action<R: Runtime>(
window: WebviewWindow<R>,
req: CallWebsocketRequestActionRequest,
@@ -1152,7 +1135,6 @@ async fn cmd_call_websocket_request_action<R: Runtime>(
.await?)
}
#[tauri::command]
async fn cmd_workspace_actions<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
@@ -1160,7 +1142,6 @@ async fn cmd_workspace_actions<R: Runtime>(
Ok(plugin_manager.get_workspace_actions(&window.plugin_context()).await?)
}
#[tauri::command]
async fn cmd_call_workspace_action<R: Runtime>(
window: WebviewWindow<R>,
req: CallWorkspaceActionRequest,
@@ -1175,7 +1156,6 @@ async fn cmd_call_workspace_action<R: Runtime>(
.await?)
}
#[tauri::command]
async fn cmd_folder_actions<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
@@ -1183,7 +1163,6 @@ async fn cmd_folder_actions<R: Runtime>(
Ok(plugin_manager.get_folder_actions(&window.plugin_context()).await?)
}
#[tauri::command]
async fn cmd_call_folder_action<R: Runtime>(
window: WebviewWindow<R>,
req: CallFolderActionRequest,
@@ -1198,7 +1177,6 @@ async fn cmd_call_folder_action<R: Runtime>(
.await?)
}
#[tauri::command]
async fn cmd_grpc_request_actions<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
@@ -1206,7 +1184,6 @@ async fn cmd_grpc_request_actions<R: Runtime>(
Ok(plugin_manager.get_grpc_request_actions(&window.plugin_context()).await?)
}
#[tauri::command]
async fn cmd_template_function_summaries<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
@@ -1215,7 +1192,6 @@ async fn cmd_template_function_summaries<R: Runtime>(
Ok(results)
}
#[tauri::command]
async fn cmd_template_function_config<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
@@ -1229,7 +1205,6 @@ async fn cmd_template_function_config<R: Runtime>(
.await?)
}
#[tauri::command]
async fn cmd_get_http_authentication_summaries<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
@@ -1239,7 +1214,6 @@ async fn cmd_get_http_authentication_summaries<R: Runtime>(
Ok(results.into_iter().map(|(_, a)| a).collect())
}
#[tauri::command]
async fn cmd_get_http_authentication_config<R: Runtime>(
window: WebviewWindow<R>,
app_handle: AppHandle<R>,
@@ -1294,7 +1268,6 @@ async fn cmd_get_http_authentication_config<R: Runtime>(
.await?)
}
#[tauri::command]
async fn cmd_call_http_request_action<R: Runtime>(
window: WebviewWindow<R>,
req: CallHttpRequestActionRequest,
@@ -1314,7 +1287,6 @@ async fn cmd_call_http_request_action<R: Runtime>(
.await?)
}
#[tauri::command]
async fn cmd_call_grpc_request_action<R: Runtime>(
window: WebviewWindow<R>,
req: CallGrpcRequestActionRequest,
@@ -1334,7 +1306,6 @@ async fn cmd_call_grpc_request_action<R: Runtime>(
.await?)
}
#[tauri::command]
async fn cmd_call_http_authentication_action<R: Runtime>(
window: WebviewWindow<R>,
app_handle: AppHandle<R>,
@@ -1390,7 +1361,6 @@ async fn cmd_call_http_authentication_action<R: Runtime>(
.await?)
}
#[tauri::command]
async fn cmd_curl_to_request<R: Runtime>(
window: WebviewWindow<R>,
command: &str,
@@ -1412,7 +1382,6 @@ async fn cmd_curl_to_request<R: Runtime>(
})?)
}
#[tauri::command]
async fn cmd_export_data<R: Runtime>(
app_handle: AppHandle<R>,
export_path: &str,
@@ -1439,7 +1408,6 @@ async fn cmd_export_data<R: Runtime>(
/// array of numbers, several times the size of the thing being saved. And the callers that need
/// this — values the editor collapsed — are holding base64 already, so passing it through
/// untouched means the save never decodes megabytes on the main thread.
#[tauri::command]
async fn cmd_save_base64_to_binary<R: Runtime>(
_app_handle: AppHandle<R>,
filepath: &str,
@@ -1453,7 +1421,6 @@ async fn cmd_save_base64_to_binary<R: Runtime>(
Ok(())
}
#[tauri::command]
async fn cmd_save_response<R: Runtime>(
app_handle: AppHandle<R>,
response_id: &str,
@@ -1468,7 +1435,6 @@ async fn cmd_save_response<R: Runtime>(
Ok(())
}
#[tauri::command]
async fn cmd_send_http_request<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
@@ -1540,7 +1506,6 @@ async fn cmd_send_http_request<R: Runtime>(
Ok(r)
}
#[tauri::command]
async fn cmd_reload_plugins<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
@@ -1553,7 +1518,6 @@ async fn cmd_reload_plugins<R: Runtime>(
Ok(errors)
}
#[tauri::command]
async fn cmd_plugin_info<R: Runtime>(
id: &str,
app_handle: AppHandle<R>,
@@ -1592,7 +1556,6 @@ fn fallback_plugin_metadata(directory: &str) -> PluginMetadata {
}
}
#[tauri::command]
async fn cmd_delete_all_grpc_connections<R: Runtime>(
request_id: &str,
app_handle: AppHandle<R>,
@@ -1604,7 +1567,6 @@ async fn cmd_delete_all_grpc_connections<R: Runtime>(
)?)
}
#[tauri::command]
async fn cmd_delete_send_history<R: Runtime>(
workspace_id: &str,
app_handle: AppHandle<R>,
@@ -1619,7 +1581,6 @@ async fn cmd_delete_send_history<R: Runtime>(
})?)
}
#[tauri::command]
async fn cmd_delete_all_http_responses<R: Runtime>(
request_id: &str,
app_handle: AppHandle<R>,
@@ -1632,7 +1593,6 @@ async fn cmd_delete_all_http_responses<R: Runtime>(
Ok(())
}
#[tauri::command]
async fn cmd_get_workspace_meta<R: Runtime>(
app_handle: AppHandle<R>,
workspace_id: &str,
@@ -1642,7 +1602,6 @@ async fn cmd_get_workspace_meta<R: Runtime>(
Ok(db.get_or_create_workspace_meta(&workspace.id)?)
}
#[tauri::command]
async fn cmd_new_child_window<R: Runtime>(
parent_window: WebviewWindow<R>,
url: &str,
@@ -1665,7 +1624,6 @@ async fn cmd_new_child_window<R: Runtime>(
Ok(())
}
#[tauri::command]
async fn cmd_new_main_window<R: Runtime>(app_handle: AppHandle<R>, url: &str) -> YaakResult<()> {
let use_native_titlebar = app_handle.db().get_settings().use_native_titlebar;
let initialization_script = initial_appearance_script(&app_handle);
@@ -1679,7 +1637,6 @@ async fn cmd_new_main_window<R: Runtime>(app_handle: AppHandle<R>, url: &str) ->
Ok(())
}
#[tauri::command]
async fn cmd_check_for_updates<R: Runtime>(
window: WebviewWindow<R>,
yaak_updater: State<'_, Mutex<YaakUpdater>>,
@@ -1770,6 +1727,10 @@ pub fn run() {
builder
.setup(|app| {
// The RPC command registry — every frontend command dispatches
// through this via the single `rpc` Tauri command
app.manage(rpc_ext::build_rpc_router::<TauriRuntime>());
// Initialize HTTP connection manager
app.manage(yaak_http::manager::HttpConnectionManager::new());
@@ -1844,128 +1805,7 @@ pub fn run() {
Ok(())
})
.invoke_handler(tauri::generate_handler![
cmd_call_http_authentication_action,
cmd_call_http_request_action,
cmd_call_websocket_request_action,
cmd_call_workspace_action,
cmd_call_folder_action,
cmd_call_grpc_request_action,
cmd_check_for_updates,
cmd_curl_to_request,
cmd_delete_all_grpc_connections,
cmd_delete_all_http_responses,
cmd_delete_send_history,
cmd_dismiss_notification,
cmd_export_data,
cmd_send_feedback,
cmd_http_request_body,
cmd_http_response_body,
cmd_format_json,
cmd_format_graphql,
cmd_get_http_authentication_summaries,
cmd_get_http_authentication_config,
cmd_get_sse_events,
cmd_get_http_response_events,
cmd_get_workspace_meta,
cmd_grpc_go,
cmd_grpc_reflect,
cmd_grpc_request_actions,
cmd_http_request_actions,
cmd_websocket_request_actions,
cmd_workspace_actions,
cmd_folder_actions,
cmd_import_data,
cmd_metadata,
cmd_new_child_window,
cmd_new_main_window,
cmd_plugin_info,
cmd_reload_plugins,
cmd_render_template,
cmd_restart,
cmd_save_base64_to_binary,
cmd_save_response,
cmd_send_ephemeral_request,
cmd_send_http_request,
cmd_template_function_config,
cmd_template_function_summaries,
cmd_template_tokens_to_string,
//
//
// Migrated commands
crate::commands::cmd_decrypt_template,
crate::commands::cmd_default_headers,
crate::commands::cmd_disable_encryption,
crate::commands::cmd_enable_encryption,
crate::commands::cmd_get_themes,
crate::commands::cmd_reveal_workspace_key,
crate::commands::cmd_secure_template,
crate::commands::cmd_set_workspace_key,
//
// Models commands
models_ext::models_delete,
models_ext::models_duplicate,
models_ext::models_get_graphql_introspection,
models_ext::models_get_settings,
models_ext::models_grpc_events,
models_ext::models_upsert,
models_ext::models_upsert_graphql_introspection,
models_ext::models_websocket_events,
models_ext::models_workspace_models,
//
// Sync commands
sync_ext::cmd_sync_calculate,
sync_ext::cmd_sync_calculate_fs,
sync_ext::cmd_sync_apply,
sync_ext::cmd_sync_watch,
//
// Git commands
git_ext::cmd_git_checkout,
git_ext::cmd_git_branch,
git_ext::cmd_git_delete_branch,
git_ext::cmd_git_delete_remote_branch,
git_ext::cmd_git_merge_branch,
git_ext::cmd_git_rename_branch,
git_ext::cmd_git_branch_info,
git_ext::cmd_git_status,
git_ext::cmd_git_worktree_status,
git_ext::cmd_git_watch_worktree_status,
git_ext::cmd_git_log,
git_ext::cmd_git_log_for_file,
git_ext::cmd_git_file_diff_for_commit,
git_ext::cmd_git_initialize,
git_ext::cmd_git_clone,
git_ext::cmd_git_commit,
git_ext::cmd_git_fetch_all,
git_ext::cmd_git_push,
git_ext::cmd_git_pull,
git_ext::cmd_git_pull_force_reset,
git_ext::cmd_git_pull_merge,
git_ext::cmd_git_add,
git_ext::cmd_git_unstage,
git_ext::cmd_git_reset_changes,
git_ext::cmd_git_restore_files,
git_ext::cmd_git_restore_file_from_commit,
git_ext::cmd_git_add_credential,
git_ext::cmd_git_remotes,
git_ext::cmd_git_add_remote,
git_ext::cmd_git_rm_remote,
//
// Plugin commands
plugins_ext::cmd_plugin_init_errors,
plugins_ext::cmd_plugins_install_from_directory,
plugins_ext::cmd_plugins_search,
plugins_ext::cmd_plugins_install,
plugins_ext::cmd_plugins_uninstall,
plugins_ext::cmd_plugins_updates,
plugins_ext::cmd_plugins_update_all,
//
// WebSocket commands
ws_ext::cmd_ws_delete_connections,
ws_ext::cmd_ws_send,
ws_ext::cmd_ws_close,
ws_ext::cmd_ws_connect,
])
.invoke_handler(tauri::generate_handler![rpc_ext::rpc])
.build(tauri::generate_context!())
.expect("error while running tauri application")
.run(|app_handle, event| {
@@ -140,7 +140,6 @@ impl<'a, R: Runtime, M: Manager<R>> BlobManagerExt<'a, R> for M {
// Commands for yaak-models
use tauri::WebviewWindow;
#[tauri::command]
pub(crate) fn models_upsert<R: Runtime>(
window: WebviewWindow<R>,
model: AnyModel,
@@ -171,7 +170,6 @@ pub(crate) fn models_upsert<R: Runtime>(
// Async so cascading deletes (e.g. a workspace with thousands of requests) run on a
// blocking thread instead of stalling the main thread and all other IPC.
#[tauri::command]
pub(crate) async fn models_delete<R: Runtime>(
window: WebviewWindow<R>,
model: AnyModel,
@@ -204,7 +202,6 @@ pub(crate) async fn models_delete<R: Runtime>(
.map_err(|e| GenericError(format!("Delete task failed: {e}")))?
}
#[tauri::command]
pub(crate) fn models_duplicate<R: Runtime>(
window: WebviewWindow<R>,
model_type: String,
@@ -238,7 +235,6 @@ pub(crate) fn models_duplicate<R: Runtime>(
})
}
#[tauri::command]
pub(crate) fn models_websocket_events<R: Runtime>(
app_handle: tauri::AppHandle<R>,
connection_id: &str,
@@ -246,7 +242,6 @@ pub(crate) fn models_websocket_events<R: Runtime>(
Ok(app_handle.db().list_websocket_events(connection_id)?)
}
#[tauri::command]
pub(crate) fn models_grpc_events<R: Runtime>(
app_handle: tauri::AppHandle<R>,
connection_id: &str,
@@ -254,12 +249,10 @@ pub(crate) fn models_grpc_events<R: Runtime>(
Ok(app_handle.db().list_grpc_events(connection_id)?)
}
#[tauri::command]
pub(crate) fn models_get_settings<R: Runtime>(app_handle: tauri::AppHandle<R>) -> Result<Settings> {
Ok(app_handle.db().get_settings())
}
#[tauri::command]
pub(crate) fn models_get_graphql_introspection<R: Runtime>(
app_handle: tauri::AppHandle<R>,
request_id: &str,
@@ -267,7 +260,6 @@ pub(crate) fn models_get_graphql_introspection<R: Runtime>(
Ok(app_handle.db().get_graphql_introspection(request_id))
}
#[tauri::command]
pub(crate) fn models_upsert_graphql_introspection<R: Runtime>(
app_handle: tauri::AppHandle<R>,
request_id: &str,
@@ -279,7 +271,6 @@ pub(crate) fn models_upsert_graphql_introspection<R: Runtime>(
Ok(app_handle.db().upsert_graphql_introspection(workspace_id, request_id, content, &source)?)
}
#[tauri::command]
pub(crate) async fn models_workspace_models<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: Option<&str>,
@@ -16,7 +16,7 @@ use std::time::{Duration, Instant};
use tauri::path::BaseDirectory;
use tauri::plugin::{Builder, TauriPlugin};
use tauri::{
AppHandle, Emitter, Manager, RunEvent, Runtime, State, WebviewWindow, WindowEvent, command,
AppHandle, Emitter, Manager, RunEvent, Runtime, State, WebviewWindow, WindowEvent,
is_dev,
};
use tokio::sync::Mutex;
@@ -132,7 +132,6 @@ impl PluginUpdater {
// Tauri Commands
// ============================================================================
#[command]
pub async fn cmd_plugins_search<R: Runtime>(
app_handle: AppHandle<R>,
query: &str,
@@ -142,7 +141,6 @@ pub async fn cmd_plugins_search<R: Runtime>(
Ok(search_plugins(&http_client, query).await?)
}
#[command]
pub async fn cmd_plugins_install<R: Runtime>(
window: WebviewWindow<R>,
name: &str,
@@ -165,7 +163,6 @@ pub async fn cmd_plugins_install<R: Runtime>(
Ok(())
}
#[command]
pub async fn cmd_plugins_install_from_directory<R: Runtime>(
window: WebviewWindow<R>,
directory: &str,
@@ -187,7 +184,6 @@ pub async fn cmd_plugins_install_from_directory<R: Runtime>(
Ok(plugin)
}
#[command]
pub async fn cmd_plugins_uninstall<R: Runtime>(
plugin_id: &str,
window: WebviewWindow<R>,
@@ -198,14 +194,12 @@ pub async fn cmd_plugins_uninstall<R: Runtime>(
Ok(delete_and_uninstall(plugin_manager, &query_manager, &plugin_context, plugin_id).await?)
}
#[command]
pub async fn cmd_plugin_init_errors(
plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<(String, String)>> {
Ok(plugin_manager.take_init_errors().await)
}
#[command]
pub async fn cmd_plugins_updates<R: Runtime>(
app_handle: AppHandle<R>,
) -> Result<PluginUpdatesResponse> {
@@ -215,7 +209,6 @@ pub async fn cmd_plugins_updates<R: Runtime>(
Ok(check_plugin_updates(&http_client, plugins).await?)
}
#[command]
pub async fn cmd_plugins_update_all<R: Runtime>(
window: WebviewWindow<R>,
) -> Result<Vec<PluginNameVersion>> {
File diff suppressed because it is too large Load Diff
+9 -17
View File
@@ -8,8 +8,7 @@ use chrono::Utc;
use log::warn;
use serde::{Deserialize, Serialize};
use std::path::Path;
use tauri::ipc::Channel;
use tauri::{AppHandle, Listener, Runtime, command};
use tauri::{AppHandle, Listener, Runtime};
use tokio::sync::watch;
use ts_rs::TS;
use yaak_sync::error::Error::InvalidSyncDirectory;
@@ -19,7 +18,6 @@ use yaak_sync::sync::{
};
use yaak_sync::watch::{WatchEvent, watch_directory};
#[command]
pub(crate) async fn cmd_sync_calculate<R: Runtime>(
app_handle: AppHandle<R>,
workspace_id: &str,
@@ -40,14 +38,12 @@ pub(crate) async fn cmd_sync_calculate<R: Runtime>(
Ok(compute_sync_ops(db_candidates, fs_candidates))
}
#[command]
pub(crate) async fn cmd_sync_calculate_fs(dir: &Path) -> Result<Vec<SyncOp>> {
let db_candidates = Vec::new();
let fs_candidates = get_fs_candidates(dir)?;
Ok(compute_sync_ops(db_candidates, fs_candidates))
}
#[command]
pub(crate) async fn cmd_sync_apply<R: Runtime>(
app_handle: AppHandle<R>,
sync_ops: Vec<SyncOp>,
@@ -68,23 +64,19 @@ pub(crate) struct WatchResult {
unlisten_event: String,
}
#[command]
pub(crate) async fn cmd_sync_watch<R: Runtime>(
pub(crate) async fn sync_watch<R, F>(
app_handle: AppHandle<R>,
sync_dir: &Path,
workspace_id: &str,
channel: Channel<WatchEvent>,
) -> Result<WatchResult> {
on_event: F,
) -> Result<WatchResult>
where
R: Runtime,
F: Fn(WatchEvent) + Send + Sync + 'static,
{
let (cancel_tx, cancel_rx) = watch::channel(());
// Create a callback that forwards events to the Tauri channel
let callback = move |event: WatchEvent| {
if let Err(e) = channel.send(event) {
warn!("Failed to send watch event: {:?}", e);
}
};
watch_directory(&sync_dir, callback, cancel_rx).await?;
watch_directory(&sync_dir, on_event, cancel_rx).await?;
let app_handle_inner = app_handle.clone();
let unlisten_event =
+1 -5
View File
@@ -9,7 +9,7 @@ use log::{debug, info, warn};
use std::str::FromStr;
use std::sync::Arc;
use tauri::http::HeaderValue;
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow, command};
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow};
use tokio::sync::{Mutex, mpsc};
use tokio_tungstenite::tungstenite::Message;
use url::Url;
@@ -29,7 +29,6 @@ use yaak_templates::{RenderErrorBehavior, RenderOptions};
use yaak_tls::find_client_certificate;
use yaak_ws::{WebsocketManager, render_websocket_request};
#[command]
pub async fn cmd_ws_delete_connections<R: Runtime>(
request_id: &str,
app_handle: AppHandle<R>,
@@ -41,7 +40,6 @@ pub async fn cmd_ws_delete_connections<R: Runtime>(
)?)
}
#[command]
pub async fn cmd_ws_send<R: Runtime>(
connection_id: &str,
environment_id: Option<&str>,
@@ -125,7 +123,6 @@ async fn send_websocket_message<R: Runtime>(
Ok(connection.clone())
}
#[command]
pub async fn cmd_ws_close<R: Runtime>(
connection_id: &str,
app_handle: AppHandle<R>,
@@ -149,7 +146,6 @@ pub async fn cmd_ws_close<R: Runtime>(
Ok(connection)
}
#[command]
pub async fn cmd_ws_connect<R: Runtime>(
request_id: &str,
environment_id: Option<&str>,
+2 -2
View File
@@ -49,13 +49,13 @@ fn setup_window_menu<R: Runtime>(win: &WebviewWindow<R>) {
}
#[tauri::command]
fn rpc(
async fn rpc(
router: State<'_, RpcRouter<ProxyCtx>>,
ctx: State<'_, ProxyCtx>,
cmd: String,
payload: serde_json::Value,
) -> Result<serde_json::Value, String> {
router.dispatch(&cmd, payload, &ctx).map_err(|e| e.message)
router.dispatch(&cmd, payload, &ctx).await.map_err(|e| e.message)
}
pub fn run() {
+5 -1
View File
@@ -8,6 +8,10 @@ export * from "./bindings/license";
const CHECK_QUERY_KEY = ["license.check"];
export async function checkLicense(): Promise<LicenseCheckStatus> {
return platform.rpc<LicenseCheckStatus>("plugin:yaak-license|check");
}
export function useLicense() {
const queryClient = useQueryClient();
const activate = useMutation<void, string, { licenseKey: string }>({
@@ -37,7 +41,7 @@ export function useLicense() {
if (!appInfo.featureLicense) {
return null;
}
return platform.rpc<LicenseCheckStatus>("plugin:yaak-license|check");
return checkLicense();
},
});
+51 -11
View File
@@ -1,10 +1,20 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::mpsc;
/// A boxed future, so handlers of different concrete types can share a map.
pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
/// Type-erased handler function: takes context + JSON payload, returns JSON or error.
///
/// Handlers are async and take the context by value: the future outlives the
/// `dispatch` call frame, so it cannot borrow, and contexts are cheap clones
/// (handles and `Arc`s). Synchronous handlers wrap into this via `rpc_handler!`
/// with no visible change.
type HandlerFn<Ctx> =
Box<dyn Fn(&Ctx, serde_json::Value) -> Result<serde_json::Value, RpcError> + Send + Sync>;
Box<dyn Fn(Ctx, serde_json::Value) -> BoxFuture<Result<serde_json::Value, RpcError>> + Send + Sync>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcError {
@@ -56,33 +66,33 @@ pub struct RpcRouter<Ctx> {
handlers: HashMap<&'static str, HandlerFn<Ctx>>,
}
impl<Ctx> RpcRouter<Ctx> {
impl<Ctx: Clone> RpcRouter<Ctx> {
pub fn new() -> Self {
Self { handlers: HashMap::new() }
}
/// Register a handler for a command name.
/// Use the `rpc_handler!` macro to wrap a typed function.
/// Use the `rpc_handler!` (sync) or `rpc_handler_async!` macro to wrap a typed function.
pub fn register(&mut self, cmd: &'static str, handler: HandlerFn<Ctx>) {
self.handlers.insert(cmd, handler);
}
/// Dispatch a command by name with a JSON payload.
pub fn dispatch(
pub async fn dispatch(
&self,
cmd: &str,
payload: serde_json::Value,
ctx: &Ctx,
) -> Result<serde_json::Value, RpcError> {
match self.handlers.get(cmd) {
Some(handler) => handler(ctx, payload),
Some(handler) => handler(ctx.clone(), payload).await,
None => Err(RpcError { message: format!("unknown command: {cmd}") }),
}
}
/// Handle a full `RpcRequest`, returning an `RpcResponse`.
pub fn handle(&self, req: RpcRequest, ctx: &Ctx) -> RpcResponse {
match self.dispatch(&req.cmd, req.payload, ctx) {
pub async fn handle(&self, req: RpcRequest, ctx: &Ctx) -> RpcResponse {
match self.dispatch(&req.cmd, req.payload, ctx).await {
Ok(payload) => RpcResponse::Success { id: req.id, payload },
Err(e) => RpcResponse::Error { id: req.id, error: e.message },
}
@@ -195,7 +205,7 @@ macro_rules! define_rpc {
};
}
/// Wrap a typed handler function into a type-erased `HandlerFn`.
/// Wrap a typed synchronous handler function into a type-erased `HandlerFn`.
///
/// The function must have the signature:
/// `fn(ctx: &Ctx, req: Req) -> Result<Res, RpcError>`
@@ -211,9 +221,39 @@ macro_rules! define_rpc {
macro_rules! rpc_handler {
($f:expr) => {
Box::new(|ctx, payload| {
let req = serde_json::from_value(payload).map_err($crate::RpcError::from)?;
let res = $f(ctx, req)?;
serde_json::to_value(res).map_err($crate::RpcError::from)
Box::pin(async move {
let req = serde_json::from_value(payload).map_err($crate::RpcError::from)?;
let res = $f(&ctx, req)?;
serde_json::to_value(res).map_err($crate::RpcError::from)
})
})
};
}
/// Wrap a typed async handler function into a type-erased `HandlerFn`.
///
/// The function must have the signature:
/// `async fn(ctx: Ctx, req: Req) -> Result<Res, E>`
/// where `Req: DeserializeOwned`, `Res: Serialize`, and `E: ToString`, so
/// handlers can keep returning their own error types.
///
/// # Example
/// ```ignore
/// async fn cmd_metadata(ctx: ClientCtx, req: MetadataReq) -> Result<AppMetaData, Error> { ... }
///
/// router.register("cmd_metadata", rpc_handler_async!(cmd_metadata));
/// ```
#[macro_export]
macro_rules! rpc_handler_async {
($f:expr) => {
Box::new(|ctx, payload| {
Box::pin(async move {
let req = serde_json::from_value(payload).map_err($crate::RpcError::from)?;
let res = $f(ctx, req)
.await
.map_err(|e| $crate::RpcError { message: e.to_string() })?;
serde_json::to_value(res).map_err($crate::RpcError::from)
})
})
};
}
+8 -7
View File
@@ -64,22 +64,23 @@ export function useGitWorktreeStatus(dir: string, refreshKey?: string) {
}
export function watchGitWorktreeStatus(dir: string, callback: (status: GitWorktreeStatus) => void) {
const unlistenPromise = platform.rpcStream<GitWatchResult, GitWorktreeStatus>(
const handle = platform.rpcStream<GitWatchResult, GitWorktreeStatus>(
"cmd_git_watch_worktree_status",
{ dir },
callback,
);
void unlistenPromise
.then(({ unlistenEvent }) => {
addGitWatchKey(unlistenEvent);
void handle
.then(({ result }) => {
addGitWatchKey(result.unlistenEvent);
})
.catch(console.debug);
return () =>
unlistenPromise
.then(async ({ unlistenEvent }) => {
unlistenGitWatcher(unlistenEvent);
handle
.then(async ({ result, unlisten }) => {
unlistenGitWatcher(result.unlistenEvent);
unlisten();
})
.catch(console.error);
}
+1
View File
@@ -5,6 +5,7 @@ edition = "2024"
publish = false
[dependencies]
ts-rs = { workspace = true }
anyhow = "1.0.97"
async-recursion = "1.1.1"
dunce = "1.0.4"
+4 -2
View File
@@ -18,15 +18,17 @@ pub fn serialize_options() -> SerializeOptions {
SerializeOptions::new().skip_default_fields(false)
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[derive(Serialize, Deserialize, Debug, Default, ts_rs::TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_grpc.ts")]
pub struct ServiceDefinition {
pub name: String,
pub methods: Vec<MethodDefinition>,
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[derive(Serialize, Deserialize, Debug, Default, ts_rs::TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_grpc.ts")]
pub struct MethodDefinition {
pub name: String,
pub schema: String,
+7 -6
View File
@@ -30,21 +30,22 @@ export function watchWorkspaceFiles(
callback: (e: WatchEvent) => void,
) {
console.log("Watching workspace files", workspaceId, syncDir);
const unlistenPromise = platform.rpcStream<WatchResult, WatchEvent>(
const handle = platform.rpcStream<WatchResult, WatchEvent>(
"cmd_sync_watch",
{ workspaceId, syncDir },
callback,
);
void unlistenPromise.then(({ unlistenEvent }) => {
addWatchKey(unlistenEvent);
void handle.then(({ result }) => {
addWatchKey(result.unlistenEvent);
});
return () =>
unlistenPromise
.then(async ({ unlistenEvent }) => {
handle
.then(async ({ result, unlisten }) => {
console.log("Unwatching workspace files", workspaceId, syncDir);
unlistenToWatcher(unlistenEvent);
unlistenToWatcher(result.unlistenEvent);
unlisten();
})
.catch(console.error);
}
+36 -16
View File
@@ -1,5 +1,5 @@
import { getIdentifier } from "@tauri-apps/api/app";
import { Channel, convertFileSrc, invoke } from "@tauri-apps/api/core";
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
import { emit as tauriEmit, listen as tauriListen } from "@tauri-apps/api/event";
import { basename, resolveResource } from "@tauri-apps/api/path";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
@@ -15,17 +15,14 @@ import type {
PlatformCapabilities,
PlatformWindow,
RpcPayload,
RpcStreamHandle,
Unsubscribe,
} from "../types";
/**
* The desktop host: the yaak-rpc envelope carried by Tauri's `invoke` and
* window events.
*
* Commands still arrive as their own `invoke` names rather than one `rpc`
* command, because the Rust side has not moved onto `RpcRouter` yet. When it
* does, only `rpc` below changes — `invoke("rpc", { cmd, payload })`, the way
* the proxy app already does it — and no call site notices.
* window events. Every command goes through the single `rpc` Tauri command
* into the `RpcRouter` on the Rust side.
*/
/**
@@ -92,6 +89,21 @@ function createWindow(): PlatformWindow {
};
}
async function rpc<T>(cmd: string, payload?: RpcPayload): Promise<T> {
try {
// Host plugin commands (`plugin:yaak-license|check`, ...) are registered by
// their Tauri plugins and ride outside the envelope. Everything else goes
// through the single `rpc` command and the RpcRouter behind it.
if (cmd.startsWith("plugin:")) {
return await invoke<T>(cmd, payload);
}
return await invoke<T>("rpc", { cmd, payload: payload ?? {} });
} catch (err) {
console.warn("Platform command error", cmd, err);
throw err;
}
}
export function createTauriPlatform(): Platform {
const window = createWindow();
@@ -119,21 +131,29 @@ export function createTauriPlatform(): Platform {
resolveResource: (path) => resolveResource(path),
},
async rpc<T>(cmd: string, payload?: RpcPayload): Promise<T> {
rpc,
async rpcStream<T, M>(
cmd: string,
payload: RpcPayload,
onMessage: (message: M) => void,
): Promise<RpcStreamHandle<T>> {
// The caller mints the stream id, and the subscription is awaited before
// the command dispatches: registration is its own IPC round trip, and the
// command may emit its first message while it runs.
const streamId = crypto.randomUUID();
const unlisten = await tauriListen<M>(`stream_${streamId}`, (e) => onMessage(e.payload), {
target: { kind: "Window", label: window.label },
});
try {
return await invoke<T>(cmd, payload);
const result = await rpc<T>(cmd, { ...payload, streamId });
return { result, unlisten };
} catch (err) {
console.warn("Platform command error", cmd, err);
unlisten();
throw err;
}
},
rpcStream<T, M>(cmd: string, payload: RpcPayload, onMessage: (message: M) => void): Promise<T> {
const channel = new Channel<M>();
channel.onmessage = onMessage;
return invoke<T>(cmd, { ...payload, channel });
},
listen<T>(event: string, callback: (payload: T) => void): Unsubscribe {
return toSyncUnsubscribe(
tauriListen<T>(event, (e) => callback(e.payload), {
+16 -4
View File
@@ -34,6 +34,12 @@ export type PlatformAppearance = "light" | "dark";
/** Command arguments. Serialized to JSON, so only JSON values belong here. */
export type RpcPayload = Record<string, unknown>;
/** A streaming command's result, plus the teardown for its subscription. */
export interface RpcStreamHandle<T> {
result: T;
unlisten: Unsubscribe;
}
export interface DialogFilter {
name: string;
extensions: string[];
@@ -156,12 +162,18 @@ export interface Platform {
rpc<T>(cmd: string, payload?: RpcPayload): Promise<T>;
/**
* Call a command that streams messages back before it resolves.
* Call a command that streams messages back while it runs.
*
* The host passes the stream to the backend under the payload's `channel` key,
* which is the shape the existing sync and git watchers already expect.
* Resolves once the command itself completes, with its result and an
* `unlisten` that tears down the local subscription. The host guarantees the
* subscription is live before the command is dispatched, so a stream that
* emits immediately cannot lose its first message.
*/
rpcStream<T, M>(cmd: string, payload: RpcPayload, onMessage: (message: M) => void): Promise<T>;
rpcStream<T, M>(
cmd: string,
payload: RpcPayload,
onMessage: (message: M) => void,
): Promise<RpcStreamHandle<T>>;
/** Subscribe to a backend event addressed to this window. */
listen<T>(event: string, callback: (payload: T) => void): Unsubscribe;