Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
0174e54c68 Bump bytes from 1.10.1 to 1.11.1
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.10.1 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.10.1...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 19:42:16 +00:00
27 changed files with 243 additions and 508 deletions

12
Cargo.lock generated
View File

@@ -689,9 +689,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.10.1"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
dependencies = [
"serde",
]
@@ -2136,9 +2136,9 @@ dependencies = [
[[package]]
name = "git2"
version = "0.20.4"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110"
dependencies = [
"bitflags 2.9.1",
"libc",
@@ -3036,9 +3036,9 @@ dependencies = [
[[package]]
name = "libgit2-sys"
version = "0.18.3+1.9.2"
version = "0.18.1+1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487"
checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e"
dependencies = [
"cc",
"libc",

View File

@@ -2,6 +2,7 @@ use crate::PluginContextExt;
use crate::error::Result;
use std::sync::Arc;
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow, command};
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
use yaak_crypto::manager::EncryptionManager;
use yaak_models::models::HttpRequestHeader;
use yaak_models::queries::workspaces::default_headers;
@@ -22,6 +23,20 @@ impl<'a, R: Runtime, M: Manager<R>> EncryptionManagerExt<'a, R> for M {
}
}
#[command]
pub(crate) async fn cmd_show_workspace_key<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: &str,
) -> Result<()> {
let key = window.crypto().reveal_workspace_key(workspace_id)?;
window
.dialog()
.message(format!("Your workspace key is \n\n{}", key))
.kind(MessageDialogKind::Info)
.show(|_v| {});
Ok(())
}
#[command]
pub(crate) async fn cmd_decrypt_template<R: Runtime>(
window: WebviewWindow<R>,

View File

@@ -9,8 +9,8 @@ use yaak_git::{
BranchDeleteResult, CloneResult, GitCommit, GitRemote, GitStatusSummary, PullResult,
PushResult, git_add, git_add_credential, git_add_remote, git_checkout_branch, git_clone,
git_commit, git_create_branch, git_delete_branch, git_delete_remote_branch, git_fetch_all,
git_init, git_log, git_merge_branch, git_pull, git_pull_force_reset, git_pull_merge, git_push,
git_remotes, git_rename_branch, git_reset_changes, git_rm_remote, git_status, git_unstage,
git_init, git_log, git_merge_branch, git_pull, git_push, git_remotes, git_rename_branch,
git_rm_remote, git_status, git_unstage,
};
// NOTE: All of these commands are async to prevent blocking work from locking up the UI
@@ -89,20 +89,6 @@ 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,
branch: &str,
) -> Result<PullResult> {
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 {
@@ -119,11 +105,6 @@ 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_add_credential(
remote_url: &str,

View File

@@ -37,8 +37,8 @@ use yaak_grpc::{Code, ServiceDefinition, serialize_message};
use yaak_mac_window::AppHandleMacWindowExt;
use yaak_models::models::{
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Plugin,
Workspace, WorkspaceMeta,
GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState,
Plugin, Workspace, WorkspaceMeta,
};
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
use yaak_plugins::events::{
@@ -1271,6 +1271,35 @@ async fn cmd_save_response<R: Runtime>(
Ok(())
}
#[tauri::command]
async fn cmd_send_folder<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
environment_id: Option<String>,
cookie_jar_id: Option<String>,
folder_id: &str,
) -> YaakResult<()> {
let requests = app_handle.db().list_http_requests_for_folder_recursive(folder_id)?;
for request in requests {
let app_handle = app_handle.clone();
let window = window.clone();
let environment_id = environment_id.clone();
let cookie_jar_id = cookie_jar_id.clone();
tokio::spawn(async move {
let _ = cmd_send_http_request(
app_handle,
window,
environment_id.as_deref(),
cookie_jar_id.as_deref(),
request,
)
.await;
});
}
Ok(())
}
#[tauri::command]
async fn cmd_send_http_request<R: Runtime>(
app_handle: AppHandle<R>,
@@ -1367,6 +1396,27 @@ async fn cmd_install_plugin<R: Runtime>(
Ok(plugin)
}
#[tauri::command]
async fn cmd_create_grpc_request<R: Runtime>(
workspace_id: &str,
name: &str,
sort_priority: f64,
folder_id: Option<&str>,
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> YaakResult<GrpcRequest> {
Ok(app_handle.db().upsert_grpc_request(
&GrpcRequest {
workspace_id: workspace_id.to_string(),
name: name.to_string(),
folder_id: folder_id.map(|s| s.to_string()),
sort_priority,
..Default::default()
},
&UpdateSource::from_window_label(window.label()),
)?)
}
#[tauri::command]
async fn cmd_reload_plugins<R: Runtime>(
app_handle: AppHandle<R>,
@@ -1629,6 +1679,7 @@ pub fn run() {
cmd_call_folder_action,
cmd_call_grpc_request_action,
cmd_check_for_updates,
cmd_create_grpc_request,
cmd_curl_to_request,
cmd_delete_all_grpc_connections,
cmd_delete_all_http_responses,
@@ -1662,6 +1713,7 @@ pub fn run() {
cmd_save_response,
cmd_send_ephemeral_request,
cmd_send_http_request,
cmd_send_folder,
cmd_template_function_config,
cmd_template_function_summaries,
cmd_template_tokens_to_string,
@@ -1676,6 +1728,7 @@ pub fn run() {
crate::commands::cmd_reveal_workspace_key,
crate::commands::cmd_secure_template,
crate::commands::cmd_set_workspace_key,
crate::commands::cmd_show_workspace_key,
//
// Models commands
models_ext::models_delete,
@@ -1709,11 +1762,8 @@ pub fn run() {
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_add_credential,
git_ext::cmd_git_remotes,
git_ext::cmd_git_add_remote,
@@ -1727,7 +1777,14 @@ pub fn run() {
plugins_ext::cmd_plugins_update_all,
//
// WebSocket commands
ws_ext::cmd_ws_upsert_request,
ws_ext::cmd_ws_duplicate_request,
ws_ext::cmd_ws_delete_request,
ws_ext::cmd_ws_delete_connection,
ws_ext::cmd_ws_delete_connections,
ws_ext::cmd_ws_list_events,
ws_ext::cmd_ws_list_requests,
ws_ext::cmd_ws_list_connections,
ws_ext::cmd_ws_send,
ws_ext::cmd_ws_close,
ws_ext::cmd_ws_connect,

View File

@@ -28,6 +28,52 @@ use yaak_templates::{RenderErrorBehavior, RenderOptions};
use yaak_tls::find_client_certificate;
use yaak_ws::{WebsocketManager, render_websocket_request};
#[command]
pub async fn cmd_ws_upsert_request<R: Runtime>(
request: WebsocketRequest,
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> Result<WebsocketRequest> {
Ok(app_handle
.db()
.upsert_websocket_request(&request, &UpdateSource::from_window_label(window.label()))?)
}
#[command]
pub async fn cmd_ws_duplicate_request<R: Runtime>(
request_id: &str,
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> Result<WebsocketRequest> {
let db = app_handle.db();
let request = db.get_websocket_request(request_id)?;
Ok(db.duplicate_websocket_request(&request, &UpdateSource::from_window_label(window.label()))?)
}
#[command]
pub async fn cmd_ws_delete_request<R: Runtime>(
request_id: &str,
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> Result<WebsocketRequest> {
Ok(app_handle.db().delete_websocket_request_by_id(
request_id,
&UpdateSource::from_window_label(window.label()),
)?)
}
#[command]
pub async fn cmd_ws_delete_connection<R: Runtime>(
connection_id: &str,
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> Result<WebsocketConnection> {
Ok(app_handle.db().delete_websocket_connection_by_id(
connection_id,
&UpdateSource::from_window_label(window.label()),
)?)
}
#[command]
pub async fn cmd_ws_delete_connections<R: Runtime>(
request_id: &str,
@@ -40,6 +86,30 @@ pub async fn cmd_ws_delete_connections<R: Runtime>(
)?)
}
#[command]
pub async fn cmd_ws_list_events<R: Runtime>(
connection_id: &str,
app_handle: AppHandle<R>,
) -> Result<Vec<WebsocketEvent>> {
Ok(app_handle.db().list_websocket_events(connection_id)?)
}
#[command]
pub async fn cmd_ws_list_requests<R: Runtime>(
workspace_id: &str,
app_handle: AppHandle<R>,
) -> Result<Vec<WebsocketRequest>> {
Ok(app_handle.db().list_websocket_requests(workspace_id)?)
}
#[command]
pub async fn cmd_ws_list_connections<R: Runtime>(
workspace_id: &str,
app_handle: AppHandle<R>,
) -> Result<Vec<WebsocketConnection>> {
Ok(app_handle.db().list_websocket_connections(workspace_id)?)
}
#[command]
pub async fn cmd_ws_send<R: Runtime>(
connection_id: &str,

View File

@@ -6,7 +6,7 @@ publish = false
[dependencies]
chrono = { workspace = true, features = ["serde"] }
git2 = { version = "0.20.4", features = ["vendored-libgit2", "vendored-openssl"] }
git2 = { version = "0.20.0", features = ["vendored-libgit2", "vendored-openssl"] }
log = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

View File

@@ -15,8 +15,8 @@ export type GitStatus = "untracked" | "conflict" | "current" | "modified" | "rem
export type GitStatusEntry = { relaPath: string, status: GitStatus, staged: boolean, prev: SyncModel | null, next: SyncModel | null, };
export type GitStatusSummary = { path: 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 GitStatusSummary = { path: string, headRef: string | null, headRefShorthand: string | null, entries: Array<GitStatusEntry>, origins: Array<string>, localBranches: Array<string>, remoteBranches: Array<string>, };
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 PullResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, };
export type PushResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, };

View File

@@ -4,7 +4,6 @@ import { createFastMutation } from '@yaakapp/app/hooks/useFastMutation';
import { queryClient } from '@yaakapp/app/lib/queryClient';
import { useMemo } from 'react';
import { BranchDeleteResult, CloneResult, GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult } from './bindings/gen_git';
import { showToast } from '@yaakapp/app/lib/toast';
export * from './bindings/gen_git';
export * from './bindings/gen_models';
@@ -14,20 +13,11 @@ export interface GitCredentials {
password: string;
}
export type DivergedStrategy = 'force_reset' | 'merge' | 'cancel';
export type UncommittedChangesStrategy = 'reset' | 'cancel';
export interface GitCallbacks {
addRemote: () => Promise<GitRemote | null>;
promptCredentials: (
result: Extract<PushResult, { type: 'needs_credentials' }>,
) => Promise<GitCredentials | null>;
promptDiverged: (
result: Extract<PullResult, { type: 'diverged' }>,
) => Promise<DivergedStrategy>;
promptUncommittedChanges: () => Promise<UncommittedChangesStrategy>;
forceSync: () => Promise<void>;
}
const onSuccess = () => queryClient.invalidateQueries({ queryKey: ['git'] });
@@ -79,15 +69,6 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
return invoke<PushResult>('cmd_git_push', { dir });
};
const handleError = (err: unknown) => {
showToast({
id: `${err}`,
message: `${err}`,
color: 'danger',
timeout: 5000,
});
}
return {
init: createFastMutation<void, string, void>({
mutationKey: ['git', 'init'],
@@ -152,9 +133,10 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
},
onSuccess,
}),
fetchAll: createFastMutation<void, string, void>({
mutationKey: ['git', 'fetch_all', dir],
fetchAll: createFastMutation<string, string, void>({
mutationKey: ['git', 'checkout', dir],
mutationFn: () => invoke('cmd_git_fetch_all', { dir }),
onSuccess,
}),
push: createFastMutation<PushResult, string, void>({
mutationKey: ['git', 'push', dir],
@@ -165,51 +147,20 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
mutationKey: ['git', 'pull', dir],
async mutationFn() {
const result = await invoke<PullResult>('cmd_git_pull', { dir });
if (result.type !== 'needs_credentials') return result;
if (result.type === 'needs_credentials') {
const creds = await callbacks.promptCredentials(result);
if (creds == null) throw new Error('Canceled');
// Needs credentials, prompt for them
const creds = await callbacks.promptCredentials(result);
if (creds == null) throw new Error('Canceled');
await invoke('cmd_git_add_credential', {
remoteUrl: result.url,
username: creds.username,
password: creds.password,
});
await invoke('cmd_git_add_credential', {
remoteUrl: result.url,
username: creds.username,
password: creds.password,
});
// Pull again after credentials
return invoke<PullResult>('cmd_git_pull', { dir });
}
if (result.type === 'uncommitted_changes') {
callbacks.promptUncommittedChanges().then(async (strategy) => {
if (strategy === 'cancel') return;
await invoke('cmd_git_reset_changes', { dir });
return invoke<PullResult>('cmd_git_pull', { dir });
}).then(async () => { onSuccess(); await callbacks.forceSync(); }, handleError);
}
if (result.type === 'diverged') {
callbacks.promptDiverged(result).then((strategy) => {
if (strategy === 'cancel') return;
if (strategy === 'force_reset') {
return invoke<PullResult>('cmd_git_pull_force_reset', {
dir,
remote: result.remote,
branch: result.branch,
});
}
return invoke<PullResult>('cmd_git_pull_merge', {
dir,
remote: result.remote,
branch: result.branch,
});
}).then(async () => { onSuccess(); await callbacks.forceSync(); }, handleError);
}
return result;
// Pull again
return invoke<PullResult>('cmd_git_pull', { dir });
},
onSuccess,
}),
@@ -218,11 +169,6 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
mutationFn: (args) => invoke('cmd_git_unstage', { dir, ...args }),
onSuccess,
}),
resetChanges: createFastMutation<void, string, void>({
mutationKey: ['git', 'reset-changes', dir],
mutationFn: () => invoke('cmd_git_reset_changes', { dir }),
onSuccess,
}),
} as const;
};

View File

@@ -13,7 +13,6 @@ mod pull;
mod push;
mod remotes;
mod repository;
mod reset;
mod status;
mod unstage;
mod util;
@@ -30,9 +29,8 @@ pub use credential::git_add_credential;
pub use fetch::git_fetch_all;
pub use init::git_init;
pub use log::{GitCommit, git_log};
pub use pull::{PullResult, git_pull, git_pull_force_reset, git_pull_merge};
pub use pull::{PullResult, git_pull};
pub use push::{PushResult, git_push};
pub use remotes::{GitRemote, git_add_remote, git_remotes, git_rm_remote};
pub use reset::git_reset_changes;
pub use status::{GitStatusSummary, git_status};
pub use unstage::git_unstage;

View File

@@ -15,23 +15,9 @@ pub enum PullResult {
Success { message: String },
UpToDate,
NeedsCredentials { url: String, error: Option<String> },
Diverged { remote: String, branch: String },
UncommittedChanges,
}
fn has_uncommitted_changes(dir: &Path) -> Result<bool> {
let repo = open_repo(dir)?;
let mut opts = git2::StatusOptions::new();
opts.include_ignored(false).include_untracked(false);
let statuses = repo.statuses(Some(&mut opts))?;
Ok(statuses.iter().any(|e| e.status() != git2::Status::CURRENT))
}
pub async fn git_pull(dir: &Path) -> Result<PullResult> {
if has_uncommitted_changes(dir)? {
return Ok(PullResult::UncommittedChanges);
}
// Extract all git2 data before any await points (git2 types are not Send)
let (branch_name, remote_name, remote_url) = {
let repo = open_repo(dir)?;
@@ -70,13 +56,6 @@ pub async fn git_pull(dir: &Path) -> Result<PullResult> {
}
if !out.status.success() {
let combined_lower = combined.to_lowercase();
if combined_lower.contains("cannot fast-forward")
|| combined_lower.contains("not possible to fast-forward")
|| combined_lower.contains("diverged")
{
return Ok(PullResult::Diverged { remote: remote_name, branch: branch_name });
}
return Err(GenericError(format!("Failed to pull {combined}")));
}
@@ -87,65 +66,6 @@ pub async fn git_pull(dir: &Path) -> Result<PullResult> {
Ok(PullResult::Success { message: format!("Pulled from {}/{}", remote_name, branch_name) })
}
pub async fn git_pull_force_reset(dir: &Path, remote: &str, branch: &str) -> Result<PullResult> {
// Step 1: fetch the remote
let fetch_out = new_binary_command(dir)
.await?
.args(["fetch", remote])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.await
.map_err(|e| GenericError(format!("failed to run git fetch: {e}")))?;
if !fetch_out.status.success() {
let stderr = String::from_utf8_lossy(&fetch_out.stderr);
return Err(GenericError(format!("Failed to fetch: {stderr}")));
}
// Step 2: reset --hard to remote/branch
let ref_name = format!("{}/{}", remote, branch);
let reset_out = new_binary_command(dir)
.await?
.args(["reset", "--hard", &ref_name])
.output()
.await
.map_err(|e| GenericError(format!("failed to run git reset: {e}")))?;
if !reset_out.status.success() {
let stderr = String::from_utf8_lossy(&reset_out.stderr);
return Err(GenericError(format!("Failed to reset: {}", stderr.trim())));
}
Ok(PullResult::Success { message: format!("Reset to {}/{}", remote, branch) })
}
pub async fn git_pull_merge(dir: &Path, remote: &str, branch: &str) -> Result<PullResult> {
let out = new_binary_command(dir)
.await?
.args(["pull", "--no-rebase", remote, branch])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.await
.map_err(|e| GenericError(format!("failed to run git pull --no-rebase: {e}")))?;
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let combined = format!("{}{}", stdout, stderr);
info!("Pull merge status={} {combined}", out.status);
if !out.status.success() {
if combined.to_lowercase().contains("conflict") {
return Err(GenericError(
"Merge conflicts detected. Please resolve them manually.".to_string(),
));
}
return Err(GenericError(format!("Failed to merge pull: {}", combined.trim())));
}
Ok(PullResult::Success { message: format!("Merged from {}/{}", remote, branch) })
}
// pub(crate) fn git_pull_old(dir: &Path) -> Result<PullResult> {
// let repo = open_repo(dir)?;
//

View File

@@ -1,20 +0,0 @@
use crate::binary::new_binary_command;
use crate::error::Error::GenericError;
use crate::error::Result;
use std::path::Path;
pub async fn git_reset_changes(dir: &Path) -> Result<()> {
let out = new_binary_command(dir)
.await?
.args(["reset", "--hard", "HEAD"])
.output()
.await
.map_err(|e| GenericError(format!("failed to run git reset: {e}")))?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(GenericError(format!("Failed to reset: {}", stderr.trim())));
}
Ok(())
}

View File

@@ -18,8 +18,6 @@ pub struct GitStatusSummary {
pub origins: Vec<String>,
pub local_branches: Vec<String>,
pub remote_branches: Vec<String>,
pub ahead: u32,
pub behind: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
@@ -162,18 +160,6 @@ pub fn git_status(dir: &Path) -> crate::error::Result<GitStatusSummary> {
let local_branches = local_branch_names(&repo)?;
let remote_branches = remote_branch_names(&repo)?;
// Compute ahead/behind relative to remote tracking branch
let (ahead, behind) = (|| -> Option<(usize, usize)> {
let head = repo.head().ok()?;
let local_oid = head.target()?;
let branch_name = head.shorthand()?;
let upstream_ref =
repo.find_branch(&format!("origin/{branch_name}"), git2::BranchType::Remote).ok()?;
let upstream_oid = upstream_ref.get().target()?;
repo.graph_ahead_behind(local_oid, upstream_oid).ok()
})()
.unwrap_or((0, 0));
Ok(GitStatusSummary {
entries,
origins,
@@ -182,7 +168,5 @@ pub fn git_status(dir: &Path) -> crate::error::Result<GitStatusSummary> {
head_ref_shorthand,
local_branches,
remote_branches,
ahead: ahead as u32,
behind: behind as u32,
})
}

View File

@@ -8,7 +8,7 @@ publish = false
async-compression = { version = "0.4", features = ["tokio", "gzip", "deflate", "brotli", "zstd"] }
async-trait = "0.1"
brotli = "7"
bytes = "1.5.0"
bytes = "1.11.1"
cookie = "0.18.1"
flate2 = "1"
futures-util = "0.3"

View File

@@ -1,5 +1,31 @@
import { invoke } from '@tauri-apps/api/core';
import { WebsocketConnection } from '@yaakapp-internal/models';
import { WebsocketConnection, WebsocketEvent, WebsocketRequest } from '@yaakapp-internal/models';
export function upsertWebsocketRequest(
request: WebsocketRequest | Partial<Omit<WebsocketRequest, 'id'>>,
) {
return invoke('cmd_ws_upsert_request', {
request,
}) as Promise<WebsocketRequest>;
}
export function duplicateWebsocketRequest(requestId: string) {
return invoke('cmd_ws_duplicate_request', {
requestId,
}) as Promise<WebsocketRequest>;
}
export function deleteWebsocketRequest(requestId: string) {
return invoke('cmd_ws_delete_request', {
requestId,
});
}
export function deleteWebsocketConnection(connectionId: string) {
return invoke('cmd_ws_delete_connection', {
connectionId,
});
}
export function deleteWebsocketConnections(requestId: string) {
return invoke('cmd_ws_delete_connections', {
@@ -7,6 +33,20 @@ export function deleteWebsocketConnections(requestId: string) {
});
}
export function listWebsocketRequests({ workspaceId }: { workspaceId: string }) {
return invoke('cmd_ws_list_requests', { workspaceId }) as Promise<WebsocketRequest[]>;
}
export function listWebsocketEvents({ connectionId }: { connectionId: string }) {
return invoke('cmd_ws_list_events', { connectionId }) as Promise<WebsocketEvent[]>;
}
export function listWebsocketConnections({ workspaceId }: { workspaceId: string }) {
return invoke('cmd_ws_list_connections', { workspaceId }) as Promise<
WebsocketConnection[]
>;
}
export function connectWebsocket({
requestId,
environmentId,

43
package-lock.json generated
View File

@@ -1414,9 +1414,9 @@
}
},
"node_modules/@hono/node-server": {
"version": "1.19.9",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
"version": "1.19.8",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.8.tgz",
"integrity": "sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
@@ -1675,12 +1675,12 @@
}
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
"version": "1.25.2",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz",
"integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==",
"license": "MIT",
"dependencies": {
"@hono/node-server": "^1.19.9",
"@hono/node-server": "^1.19.7",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"content-type": "^1.0.5",
@@ -1688,15 +1688,14 @@
"cross-spawn": "^7.0.5",
"eventsource": "^3.0.2",
"eventsource-parser": "^3.0.0",
"express": "^5.2.1",
"express-rate-limit": "^8.2.1",
"hono": "^4.11.4",
"jose": "^6.1.3",
"express": "^5.0.1",
"express-rate-limit": "^7.5.0",
"jose": "^6.1.1",
"json-schema-typed": "^8.0.2",
"pkce-challenge": "^5.0.0",
"raw-body": "^3.0.0",
"zod": "^3.25 || ^4.0",
"zod-to-json-schema": "^3.25.1"
"zod-to-json-schema": "^3.25.0"
},
"engines": {
"node": ">=18"
@@ -6866,13 +6865,10 @@
}
},
"node_modules/express-rate-limit": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
"license": "MIT",
"dependencies": {
"ip-address": "10.0.1"
},
"engines": {
"node": ">= 16"
},
@@ -8139,15 +8135,6 @@
"node": ">= 0.4"
}
},
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/ip-bigint": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ip-bigint/-/ip-bigint-7.3.0.tgz",
@@ -15794,7 +15781,7 @@
"dependencies": {
"@hono/mcp": "^0.2.3",
"@hono/node-server": "^1.19.7",
"@modelcontextprotocol/sdk": "^1.26.0",
"@modelcontextprotocol/sdk": "^1.25.2",
"hono": "^4.11.7",
"zod": "^3.25.76"
},

View File

@@ -788,12 +788,12 @@ export class PluginInstance {
const { folders } = await this.#sendForReply<ListFoldersResponse>(context, payload);
return folders.find((f) => f.id === args.id) ?? null;
},
create: async ({ name, ...args }) => {
create: async (args) => {
const payload = {
type: 'upsert_model_request',
model: {
name: '',
...args,
name: name ?? '',
id: '',
model: 'folder',
},

View File

@@ -17,7 +17,7 @@
"dependencies": {
"@hono/mcp": "^0.2.3",
"@hono/node-server": "^1.19.7",
"@modelcontextprotocol/sdk": "^1.26.0",
"@modelcontextprotocol/sdk": "^1.25.2",
"hono": "^4.11.7",
"zod": "^3.25.76"
},

View File

@@ -10,7 +10,7 @@ export const plugin: PluginDefinition = {
async onSelect(ctx, args) {
const rendered_request = await ctx.httpRequest.render({
httpRequest: args.httpRequest,
purpose: 'send',
purpose: 'preview',
});
const data = await convertToCurl(rendered_request);
await ctx.clipboard.copyText(data);

View File

@@ -239,7 +239,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
<HttpMultipartViewer response={activeResponse} />
) : mimeType?.match(/pdf/i) ? (
<EnsureCompleteResponse response={activeResponse} Component={PdfViewer} />
) : mimeType?.match(/csv|tab-separated/i) && viewMode === 'pretty' ? (
) : mimeType?.match(/csv|tab-separated/i) ? (
<HttpCsvViewer className="pb-2" response={activeResponse} />
) : (
<HTMLOrTextViewer

View File

@@ -111,7 +111,6 @@ import {
RefreshCcwIcon,
RefreshCwIcon,
RocketIcon,
RotateCcwIcon,
Rows2Icon,
SaveIcon,
SearchIcon,
@@ -250,7 +249,6 @@ const icons = {
puzzle: PuzzleIcon,
refresh: RefreshCwIcon,
rocket: RocketIcon,
rotate_ccw: RotateCcwIcon,
rows_2: Rows2Icon,
save: SaveIcon,
search: SearchIcon,

View File

@@ -1,66 +0,0 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
export interface RadioCardOption<T extends string> {
value: T;
label: ReactNode;
description?: ReactNode;
}
export interface RadioCardsProps<T extends string> {
value: T | null;
onChange: (value: T) => void;
options: RadioCardOption<T>[];
name: string;
}
export function RadioCards<T extends string>({
value,
onChange,
options,
name,
}: RadioCardsProps<T>) {
return (
<div className="flex flex-col gap-2">
{options.map((option) => {
const selected = value === option.value;
return (
<label
key={option.value}
className={classNames(
'flex items-start gap-3 p-3 rounded-lg border cursor-pointer',
'transition-colors',
selected
? 'border-border-focus'
: 'border-border-subtle hocus:border-text-subtlest',
)}
>
<input
type="radio"
name={name}
value={option.value}
checked={selected}
onChange={() => onChange(option.value)}
className="sr-only"
/>
<div
className={classNames(
'mt-1 w-4 h-4 flex-shrink-0 rounded-full border',
'flex items-center justify-center',
selected ? 'border-focus' : 'border-border',
)}
>
{selected && <div className="w-2 h-2 rounded-full bg-text" />}
</div>
<div className="flex flex-col gap-0.5">
<span className="font-semibold text-text">{option.label}</span>
{option.description && (
<span className="text-sm text-text-subtle">{option.description}</span>
)}
</div>
</label>
);
})}
</div>
);
}

View File

@@ -20,7 +20,7 @@ import { InlineCode } from '../core/InlineCode';
import { gitCallbacks } from './callbacks';
import { GitCommitDialog } from './GitCommitDialog';
import { GitRemotesDialog } from './GitRemotesDialog';
import { handlePullResult, handlePushResult } from './git-util';
import { handlePullResult } from './git-util';
import { HistoryDialog } from './HistoryDialog';
export function GitDropdown() {
@@ -48,7 +48,6 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
push,
pull,
checkout,
resetChanges,
init,
},
] = useGit(syncDir, gitCallbacks(syncDir));
@@ -73,9 +72,6 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
}
const currentBranch = status.data.headRefShorthand;
const hasChanges = status.data.entries.some((e) => e.status !== 'current');
const hasRemotes = (status.data.origins ?? []).length > 0;
const { ahead, behind } = status.data;
const tryCheckout = (branch: string, force: boolean) => {
checkout.mutate(
@@ -172,13 +168,12 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
{ type: 'separator' },
{
label: 'Push',
disabled: !hasRemotes || ahead === 0,
leftSlot: <Icon icon="arrow_up_from_line" />,
waitForOnSelect: true,
async onSelect() {
await push.mutateAsync(undefined, {
disableToastError: true,
onSuccess: handlePushResult,
onSuccess: handlePullResult,
onError(err) {
showErrorToast({
id: 'git-push-error',
@@ -191,7 +186,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
},
{
label: 'Pull',
disabled: !hasRemotes || behind === 0,
hidden: (status.data?.origins ?? []).length === 0,
leftSlot: <Icon icon="arrow_down_to_line" />,
waitForOnSelect: true,
async onSelect() {
@@ -210,7 +205,6 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
},
{
label: 'Commit...',
disabled: !hasChanges,
leftSlot: <Icon icon="git_commit_vertical" />,
onSelect() {
showDialog({
@@ -224,41 +218,6 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
});
},
},
{
label: 'Reset Changes',
hidden: !hasChanges,
leftSlot: <Icon icon="rotate_ccw" />,
color: 'danger',
async onSelect() {
const confirmed = await showConfirm({
id: 'git-reset-changes',
title: 'Reset Changes',
description: 'This will discard all uncommitted changes. This cannot be undone.',
confirmText: 'Reset',
color: 'danger',
});
if (!confirmed) return;
await resetChanges.mutateAsync(undefined, {
disableToastError: true,
onSuccess() {
showToast({
id: 'git-reset-success',
message: 'Changes have been reset',
color: 'success',
});
sync({ force: true });
},
onError(err) {
showErrorToast({
id: 'git-reset-error',
title: 'Error resetting changes',
message: String(err),
});
},
});
},
},
{ type: 'separator', label: 'Branches', hidden: localBranches.length < 1 },
...localBranches.map((branch) => {
const isCurrent = currentBranch === branch;
@@ -504,14 +463,8 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
return (
<Dropdown fullWidth items={items} onOpen={fetchAll.mutate}>
<GitMenuButton>
<InlineCode className="flex items-center gap-1">
<Icon icon="git_branch" size="xs" className="opacity-50" />
{currentBranch}
</InlineCode>
<div className="flex items-center gap-1.5">
{ahead > 0 && <span className="text-xs flex items-center gap-0.5"><span className="text-primary"></span>{ahead}</span>}
{behind > 0 && <span className="text-xs flex items-center gap-0.5"><span className="text-info"></span>{behind}</span>}
</div>
<InlineCode>{currentBranch}</InlineCode>
<Icon icon="git_branch" size="sm" />
</GitMenuButton>
</Dropdown>
);

View File

@@ -1,8 +1,5 @@
import type { GitCallbacks } from '@yaakapp-internal/git';
import { sync } from '../../init/sync';
import { promptCredentials } from './credentials';
import { promptDivergedStrategy } from './diverged';
import { promptUncommittedChangesStrategy } from './uncommitted';
import { addGitRemote } from './showAddRemoteDialog';
export function gitCallbacks(dir: string): GitCallbacks {
@@ -15,12 +12,5 @@ export function gitCallbacks(dir: string): GitCallbacks {
if (creds == null) throw new Error('Cancelled credentials prompt');
return creds;
},
promptDiverged: async ({ remote, branch }) => {
return promptDivergedStrategy({ remote, branch });
},
promptUncommittedChanges: async () => {
return promptUncommittedChangesStrategy();
},
forceSync: () => sync({ force: true }),
};
}

View File

@@ -1,102 +0,0 @@
import type { DivergedStrategy } from '@yaakapp-internal/git';
import { useState } from 'react';
import { showDialog } from '../../lib/dialog';
import { Button } from '../core/Button';
import { InlineCode } from '../core/InlineCode';
import { RadioCards } from '../core/RadioCards';
import { HStack } from '../core/Stacks';
type Resolution = 'force_reset' | 'merge';
const resolutionLabel: Record<Resolution, string> = {
force_reset: 'Force Pull',
merge: 'Merge',
};
interface DivergedDialogProps {
remote: string;
branch: string;
onResult: (strategy: DivergedStrategy) => void;
onHide: () => void;
}
function DivergedDialog({ remote, branch, onResult, onHide }: DivergedDialogProps) {
const [selected, setSelected] = useState<Resolution | null>(null);
const handleSubmit = () => {
if (selected == null) return;
onResult(selected);
onHide();
};
const handleCancel = () => {
onResult('cancel');
onHide();
};
return (
<div className="flex flex-col gap-4 mb-4">
<p className="text-text-subtle">
Your local branch has diverged from{' '}
<InlineCode>
{remote}/{branch}
</InlineCode>. How would you like to resolve this?
</p>
<RadioCards
name="diverged-strategy"
value={selected}
onChange={setSelected}
options={[
{
value: 'merge',
label: 'Merge Commit',
description: 'Combining local and remote changes into a single merge commit',
},
{
value: 'force_reset',
label: 'Force Pull',
description: 'Discard local commits and reset to match the remote branch',
},
]}
/>
<HStack space={2} justifyContent="start" className="flex-row-reverse">
<Button
color={selected === 'force_reset' ? 'danger' : 'primary'}
disabled={selected == null}
onClick={handleSubmit}
>
{selected != null ? resolutionLabel[selected] : 'Select an option'}
</Button>
<Button variant="border" onClick={handleCancel}>
Cancel
</Button>
</HStack>
</div>
);
}
export async function promptDivergedStrategy({
remote,
branch,
}: {
remote: string;
branch: string;
}): Promise<DivergedStrategy> {
return new Promise((resolve) => {
showDialog({
id: 'git-diverged',
title: 'Branches Diverged',
hideX: true,
size: 'sm',
disableBackdropClose: true,
onClose: () => resolve('cancel'),
render: ({ hide }) =>
DivergedDialog({
remote,
branch,
onHide: hide,
onResult: resolve,
}),
});
});
}

View File

@@ -26,11 +26,5 @@ export function handlePullResult(r: PullResult) {
case 'up_to_date':
showToast({ id: 'pull-nothing', message: 'Already up-to-date', color: 'info' });
break;
case 'diverged':
// Handled by mutation callback before reaching here
break;
case 'uncommitted_changes':
// Handled by mutation callback before reaching here
break;
}
}

View File

@@ -1,13 +0,0 @@
import type { UncommittedChangesStrategy } from '@yaakapp-internal/git';
import { showConfirm } from '../../lib/confirm';
export async function promptUncommittedChangesStrategy(): Promise<UncommittedChangesStrategy> {
const confirmed = await showConfirm({
id: 'git-uncommitted-changes',
title: 'Uncommitted Changes',
description: 'You have uncommitted changes. Commit or reset your changes before pulling.',
confirmText: 'Reset and Pull',
color: 'danger',
});
return confirmed ? 'reset' : 'cancel';
}

View File

@@ -9,6 +9,7 @@ type TauriCmd =
| 'cmd_call_workspace_action'
| 'cmd_call_folder_action'
| 'cmd_check_for_updates'
| 'cmd_create_grpc_request'
| 'cmd_curl_to_request'
| 'cmd_decrypt_template'
| 'cmd_default_headers'
@@ -47,7 +48,9 @@ type TauriCmd =
| 'cmd_save_response'
| 'cmd_secure_template'
| 'cmd_send_ephemeral_request'
| 'cmd_send_folder'
| 'cmd_send_http_request'
| 'cmd_show_workspace_key'
| 'cmd_template_function_summaries'
| 'cmd_template_function_config'
| 'cmd_template_tokens_to_string';