From 543325613b5dab4cb9269b96b911d6d70f8dbbc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:24:04 -0800 Subject: [PATCH 01/10] Bump hono from 4.11.10 to 4.12.4 (#416) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- plugins-external/mcp-server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f5b32ba5..95b6a06f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8008,9 +8008,9 @@ } }, "node_modules/hono": { - "version": "4.11.10", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.10.tgz", - "integrity": "sha512-kyWP5PAiMooEvGrA9jcD3IXF7ATu8+o7B3KCbPXid5se52NPqnOpM/r9qeW2heMnOekF4kqR1fXJqCYeCLKrZg==", + "version": "4.12.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.4.tgz", + "integrity": "sha512-ooiZW1Xy8rQ4oELQ++otI2T9DsKpV0M6c6cO6JGx4RTfav9poFFLlet9UMXHZnoM1yG0HWGlQLswBGX3RZmHtg==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -16066,7 +16066,7 @@ "@hono/mcp": "^0.2.3", "@hono/node-server": "^1.19.7", "@modelcontextprotocol/sdk": "^1.26.0", - "hono": "^4.11.10", + "hono": "^4.12.4", "zod": "^3.25.76" }, "devDependencies": { diff --git a/plugins-external/mcp-server/package.json b/plugins-external/mcp-server/package.json index 6d7e6581..549e1176 100644 --- a/plugins-external/mcp-server/package.json +++ b/plugins-external/mcp-server/package.json @@ -18,7 +18,7 @@ "@hono/mcp": "^0.2.3", "@hono/node-server": "^1.19.7", "@modelcontextprotocol/sdk": "^1.26.0", - "hono": "^4.11.10", + "hono": "^4.12.4", "zod": "^3.25.76" }, "devDependencies": { From 67a3dd15ac8e6403d826cc5c555c063d08eb08d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:24:21 -0800 Subject: [PATCH 02/10] Bump @hono/node-server from 1.19.9 to 1.19.10 (#417) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- plugins-external/mcp-server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95b6a06f..27715ed7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1480,9 +1480,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.10", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.10.tgz", + "integrity": "sha512-hZ7nOssGqRgyV3FVVQdfi+U4q02uB23bpnYpdvNXkYTRRyWx84b7yf1ans+dnJ/7h41sGL3CeQTfO+ZGxuO+Iw==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -16064,7 +16064,7 @@ "version": "0.2.1", "dependencies": { "@hono/mcp": "^0.2.3", - "@hono/node-server": "^1.19.7", + "@hono/node-server": "^1.19.10", "@modelcontextprotocol/sdk": "^1.26.0", "hono": "^4.12.4", "zod": "^3.25.76" diff --git a/plugins-external/mcp-server/package.json b/plugins-external/mcp-server/package.json index 549e1176..ebeba65e 100644 --- a/plugins-external/mcp-server/package.json +++ b/plugins-external/mcp-server/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@hono/mcp": "^0.2.3", - "@hono/node-server": "^1.19.7", + "@hono/node-server": "^1.19.10", "@modelcontextprotocol/sdk": "^1.26.0", "hono": "^4.12.4", "zod": "^3.25.76" From 242f55b609d39c82d7ed2692aed3c80a5371a83b Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 5 Mar 2026 14:54:52 -0800 Subject: [PATCH 03/10] Fix macOS Tahoe stoplight positioning and build on macOS 26 On macOS Tahoe (26+), the default title bar is 32px with 14px buttons, so the old formula (button_height + PAD_Y = 14 + 18 = 32) produced no change. Add TITLEBAR_EXTRA_HEIGHT to push the title bar taller than the Tahoe default. Use OnceLock to capture the original default height so repeated calls don't accumulate extra pixels. Also update CI runners to macos-26 for Tahoe SDK builds and adjust frontend padding for larger stoplights. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release-app.yml | 4 ++-- crates-tauri/yaak-mac-window/src/mac.rs | 28 ++++++++++++++++++++++--- src-web/components/HeaderSize.tsx | 2 +- src-web/lib/constants.ts | 2 +- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-app.yml b/.github/workflows/release-app.yml index d3a73d5c..c3432afb 100644 --- a/.github/workflows/release-app.yml +++ b/.github/workflows/release-app.yml @@ -13,12 +13,12 @@ jobs: fail-fast: false matrix: include: - - platform: "macos-latest" # for Arm-based Macs (M1 and above). + - platform: "macos-26" # for Arm-based Macs (M1 and above). args: "--target aarch64-apple-darwin" yaak_arch: "arm64" os: "macos" targets: "aarch64-apple-darwin" - - platform: "macos-latest" # for Intel-based Macs. + - platform: "macos-26-intel" # for Intel-based Macs. args: "--target x86_64-apple-darwin" yaak_arch: "x64" os: "macos" diff --git a/crates-tauri/yaak-mac-window/src/mac.rs b/crates-tauri/yaak-mac-window/src/mac.rs index 4be6ab43..c5b5cdba 100644 --- a/crates-tauri/yaak-mac-window/src/mac.rs +++ b/crates-tauri/yaak-mac-window/src/mac.rs @@ -12,6 +12,11 @@ unsafe impl Sync for UnsafeWindowHandle {} const WINDOW_CONTROL_PAD_X: f64 = 13.0; const WINDOW_CONTROL_PAD_Y: f64 = 18.0; +/// Extra pixels to add to the title bar height when the default title bar is +/// already as tall as button_height + PAD_Y (i.e. macOS Tahoe 26+, where the +/// default is 32px and 14 + 18 = 32). On pre-Tahoe this is unused because the +/// default title bar is shorter than button_height + PAD_Y. +const TITLEBAR_EXTRA_HEIGHT: f64 = 4.0; const MAIN_WINDOW_PREFIX: &str = "main_"; pub(crate) fn update_window_title(window: Window, title: String) { @@ -95,12 +100,29 @@ fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64, ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton); let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton); - let title_bar_container_view = close.superview().superview(); - let close_rect: NSRect = msg_send![close, frame]; let button_height = close_rect.size.height; - let title_bar_frame_height = button_height + y; + let title_bar_container_view = close.superview().superview(); + + // Capture the OS default title bar height on the first call, before + // we've modified it. This avoids the height growing on repeated calls. + use std::sync::OnceLock; + static DEFAULT_TITLEBAR_HEIGHT: OnceLock = OnceLock::new(); + let default_height = + *DEFAULT_TITLEBAR_HEIGHT.get_or_init(|| NSView::frame(title_bar_container_view).size.height); + + // On pre-Tahoe, button_height + y is larger than the default title bar + // height, so the resize works as before. On Tahoe (26+), the default is + // already 32px and button_height + y = 32, so nothing changes. In that + // case, add TITLEBAR_EXTRA_HEIGHT extra pixels to push the buttons down. + let desired = button_height + y; + let title_bar_frame_height = if desired > default_height { + desired + } else { + default_height + TITLEBAR_EXTRA_HEIGHT + }; + let mut title_bar_rect = NSView::frame(title_bar_container_view); title_bar_rect.size.height = title_bar_frame_height; title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height; diff --git a/src-web/components/HeaderSize.tsx b/src-web/components/HeaderSize.tsx index 03a951ab..5944e011 100644 --- a/src-web/components/HeaderSize.tsx +++ b/src-web/components/HeaderSize.tsx @@ -40,7 +40,7 @@ export function HeaderSize({ } else if (type() === 'macos') { if (!isFullscreen) { // Add large padding for window controls - s.paddingLeft = 72 / settings.interfaceScale; + s.paddingLeft = 76 / settings.interfaceScale; } } else if (!ignoreControlsSpacing && !settings.hideWindowControls) { s.paddingRight = WINDOW_CONTROLS_WIDTH; diff --git a/src-web/lib/constants.ts b/src-web/lib/constants.ts index 14eab995..67dbaf8c 100644 --- a/src-web/lib/constants.ts +++ b/src-web/lib/constants.ts @@ -1,4 +1,4 @@ -export const HEADER_SIZE_MD = '27px'; +export const HEADER_SIZE_MD = '30px'; export const HEADER_SIZE_LG = '40px'; export const WINDOW_CONTROLS_WIDTH = '10.5rem'; From 267508e533adb70749988f9872c575b528051746 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 5 Mar 2026 15:05:09 -0800 Subject: [PATCH 04/10] Support comments in JSON body (#419) Co-authored-by: Claude Opus 4.6 --- Cargo.lock | 1 + crates-cli/yaak-cli/src/plugin_events.rs | 64 +--- crates-tauri/yaak-app/src/lib.rs | 3 + crates-tauri/yaak-app/src/render.rs | 64 +--- crates-tauri/yaak-app/src/ws_ext.rs | 7 +- crates/yaak-common/src/serde.rs | 7 + crates/yaak-http/Cargo.toml | 1 + crates/yaak-http/src/types.rs | 132 +++++++- crates/yaak-templates/src/format_json.rs | 241 ++++++++++++- crates/yaak-templates/src/lib.rs | 1 + .../yaak-templates/src/strip_json_comments.rs | 318 ++++++++++++++++++ crates/yaak/src/render.rs | 60 +++- package-lock.json | 11 + src-web/components/GrpcEditor.tsx | 4 +- src-web/components/HttpRequestPane.tsx | 13 +- src-web/components/JsonBodyEditor.tsx | 122 +++++++ src-web/components/core/Editor/Editor.tsx | 5 + src-web/components/core/Editor/extensions.ts | 17 +- src-web/components/core/Editor/json-lint.ts | 12 +- src-web/components/graphql/GraphQLEditor.tsx | 1 + src-web/lib/jsonComments.ts | 30 ++ src-web/package.json | 1 + 22 files changed, 954 insertions(+), 161 deletions(-) create mode 100644 crates/yaak-templates/src/strip_json_comments.rs create mode 100644 src-web/components/JsonBodyEditor.tsx create mode 100644 src-web/lib/jsonComments.ts diff --git a/Cargo.lock b/Cargo.lock index 5a06b414..f822e0d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10399,6 +10399,7 @@ dependencies = [ "urlencoding", "yaak-common", "yaak-models", + "yaak-templates", "yaak-tls", "zstd", ] diff --git a/crates-cli/yaak-cli/src/plugin_events.rs b/crates-cli/yaak-cli/src/plugin_events.rs index 1e7f9f36..cadc42dc 100644 --- a/crates-cli/yaak-cli/src/plugin_events.rs +++ b/crates-cli/yaak-cli/src/plugin_events.rs @@ -3,7 +3,7 @@ use arboard::Clipboard; use console::Term; use inquire::{Confirm, Editor, Password, PasswordDisplayMode, Select, Text}; use serde_json::Value; -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; use std::io::IsTerminal; use std::path::PathBuf; use std::sync::Arc; @@ -11,11 +11,11 @@ use tokio::task::JoinHandle; use yaak::plugin_events::{ GroupedPluginEvent, HostRequest, SharedPluginEventContext, handle_shared_plugin_event, }; -use yaak::render::render_http_request; +use yaak::render::{render_grpc_request, render_http_request}; use yaak::send::{SendHttpRequestWithPluginsParams, send_http_request_with_plugins}; use yaak_crypto::manager::EncryptionManager; use yaak_models::blob_manager::BlobManager; -use yaak_models::models::{Environment, GrpcRequest, HttpRequestHeader}; +use yaak_models::models::Environment; use yaak_models::queries::any_request::AnyRequest; use yaak_models::query_manager::QueryManager; use yaak_models::render::make_vars_hashmap; @@ -29,7 +29,7 @@ use yaak_plugins::events::{ }; use yaak_plugins::manager::PluginManager; use yaak_plugins::template_callback::PluginTemplateCallback; -use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw}; +use yaak_templates::{RenderOptions, TemplateCallback, render_json_value_raw}; pub struct CliPluginEventBridge { rx_id: String, @@ -269,7 +269,7 @@ async fn build_plugin_reply( ); let render_options = RenderOptions::throw(); - match render_grpc_request_for_cli( + match render_grpc_request( &grpc_request, environment_chain, &template_callback, @@ -532,60 +532,6 @@ async fn render_json_value_for_cli( render_json_value_raw(value, vars, cb, opt).await } -async fn render_grpc_request_for_cli( - grpc_request: &GrpcRequest, - environment_chain: Vec, - cb: &T, - opt: &RenderOptions, -) -> yaak_templates::error::Result { - let vars = &make_vars_hashmap(environment_chain); - - let mut metadata = Vec::new(); - for p in grpc_request.metadata.clone() { - if !p.enabled { - continue; - } - metadata.push(HttpRequestHeader { - enabled: p.enabled, - name: parse_and_render(p.name.as_str(), vars, cb, opt).await?, - value: parse_and_render(p.value.as_str(), vars, cb, opt).await?, - id: p.id, - }) - } - - let authentication = { - let mut disabled = false; - let mut auth = BTreeMap::new(); - match grpc_request.authentication.get("disabled") { - Some(Value::Bool(true)) => { - disabled = true; - } - Some(Value::String(tmpl)) => { - disabled = parse_and_render(tmpl.as_str(), vars, cb, opt) - .await - .unwrap_or_default() - .is_empty(); - } - _ => {} - } - if disabled { - auth.insert("disabled".to_string(), Value::Bool(true)); - } else { - for (k, v) in grpc_request.authentication.clone() { - if k == "disabled" { - auth.insert(k, Value::Bool(false)); - } else { - auth.insert(k, render_json_value_raw(v, vars, cb, opt).await?); - } - } - } - auth - }; - - let url = parse_and_render(grpc_request.url.as_str(), vars, cb, opt).await?; - - Ok(GrpcRequest { url, metadata, authentication, ..grpc_request.to_owned() }) -} fn parse_cookie_name_value(raw_cookie: &str) -> Option<(String, String)> { let first_part = raw_cookie.split(';').next()?.trim(); diff --git a/crates-tauri/yaak-app/src/lib.rs b/crates-tauri/yaak-app/src/lib.rs index 480995dd..23d357a1 100644 --- a/crates-tauri/yaak-app/src/lib.rs +++ b/crates-tauri/yaak-app/src/lib.rs @@ -34,6 +34,7 @@ use tokio::time; use yaak_common::command::new_checked_command; use yaak_crypto::manager::EncryptionManager; use yaak_grpc::manager::{GrpcConfig, GrpcHandle}; +use yaak_templates::strip_json_comments::strip_json_comments; use yaak_grpc::{Code, ServiceDefinition, serialize_message}; use yaak_mac_window::AppHandleMacWindowExt; use yaak_models::models::{ @@ -433,6 +434,7 @@ async fn cmd_grpc_go( result.expect("Failed to render template") }) }); + let msg = strip_json_comments(&msg); in_msg_tx.try_send(msg.clone()).unwrap(); } Ok(IncomingMsg::Commit) => { @@ -468,6 +470,7 @@ async fn cmd_grpc_go( &RenderOptions { error_behavior: RenderErrorBehavior::Throw }, ) .await?; + let msg = strip_json_comments(&msg); app_handle.db().upsert_grpc_event( &GrpcEvent { diff --git a/crates-tauri/yaak-app/src/render.rs b/crates-tauri/yaak-app/src/render.rs index 8cb3fd00..a6abe0d3 100644 --- a/crates-tauri/yaak-app/src/render.rs +++ b/crates-tauri/yaak-app/src/render.rs @@ -1,8 +1,6 @@ -use log::info; use serde_json::Value; -use std::collections::BTreeMap; -pub use yaak::render::render_http_request; -use yaak_models::models::{Environment, GrpcRequest, HttpRequestHeader}; +pub use yaak::render::{render_grpc_request, render_http_request}; +use yaak_models::models::Environment; use yaak_models::render::make_vars_hashmap; use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw}; @@ -25,61 +23,3 @@ pub async fn render_json_value( let vars = &make_vars_hashmap(environment_chain); render_json_value_raw(value, vars, cb, opt).await } - -pub async fn render_grpc_request( - r: &GrpcRequest, - environment_chain: Vec, - cb: &T, - opt: &RenderOptions, -) -> yaak_templates::error::Result { - let vars = &make_vars_hashmap(environment_chain); - - let mut metadata = Vec::new(); - for p in r.metadata.clone() { - if !p.enabled { - continue; - } - metadata.push(HttpRequestHeader { - enabled: p.enabled, - name: parse_and_render(p.name.as_str(), vars, cb, &opt).await?, - value: parse_and_render(p.value.as_str(), vars, cb, &opt).await?, - id: p.id, - }) - } - - let authentication = { - let mut disabled = false; - let mut auth = BTreeMap::new(); - match r.authentication.get("disabled") { - Some(Value::Bool(true)) => { - disabled = true; - } - Some(Value::String(tmpl)) => { - disabled = parse_and_render(tmpl.as_str(), vars, cb, &opt) - .await - .unwrap_or_default() - .is_empty(); - info!( - "Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\"" - ); - } - _ => {} - } - if disabled { - auth.insert("disabled".to_string(), Value::Bool(true)); - } else { - for (k, v) in r.authentication.clone() { - if k == "disabled" { - auth.insert(k, Value::Bool(false)); - } else { - auth.insert(k, render_json_value_raw(v, vars, cb, &opt).await?); - } - } - } - auth - }; - - let url = parse_and_render(r.url.as_str(), vars, cb, &opt).await?; - - Ok(GrpcRequest { url, metadata, authentication, ..r.to_owned() }) -} diff --git a/crates-tauri/yaak-app/src/ws_ext.rs b/crates-tauri/yaak-app/src/ws_ext.rs index 28580f93..96b6f438 100644 --- a/crates-tauri/yaak-app/src/ws_ext.rs +++ b/crates-tauri/yaak-app/src/ws_ext.rs @@ -24,6 +24,7 @@ use yaak_models::util::UpdateSource; use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose}; use yaak_plugins::manager::PluginManager; use yaak_plugins::template_callback::PluginTemplateCallback; +use yaak_templates::strip_json_comments::maybe_strip_json_comments; use yaak_templates::{RenderErrorBehavior, RenderOptions}; use yaak_tls::find_client_certificate; use yaak_ws::{WebsocketManager, render_websocket_request}; @@ -72,8 +73,10 @@ pub async fn cmd_ws_send( ) .await?; + let message = maybe_strip_json_comments(&request.message); + let mut ws_manager = ws_manager.lock().await; - ws_manager.send(&connection.id, Message::Text(request.message.clone().into())).await?; + ws_manager.send(&connection.id, Message::Text(message.clone().into())).await?; app_handle.db().upsert_websocket_event( &WebsocketEvent { @@ -82,7 +85,7 @@ pub async fn cmd_ws_send( workspace_id: connection.workspace_id.clone(), is_server: false, message_type: WebsocketEventType::Text, - message: request.message.into(), + message: message.into(), ..Default::default() }, &UpdateSource::from_window_label(window.label()), diff --git a/crates/yaak-common/src/serde.rs b/crates/yaak-common/src/serde.rs index 683cc25d..49146aaa 100644 --- a/crates/yaak-common/src/serde.rs +++ b/crates/yaak-common/src/serde.rs @@ -21,3 +21,10 @@ pub fn get_str_map<'a>(v: &'a BTreeMap, key: &str) -> &'a str { Some(v) => v.as_str().unwrap_or_default(), } } + +pub fn get_bool_map(v: &BTreeMap, key: &str, fallback: bool) -> bool { + match v.get(key) { + None => fallback, + Some(v) => v.as_bool().unwrap_or(fallback), + } +} diff --git a/crates/yaak-http/Cargo.toml b/crates/yaak-http/Cargo.toml index 20183503..34ebba4b 100644 --- a/crates/yaak-http/Cargo.toml +++ b/crates/yaak-http/Cargo.toml @@ -29,4 +29,5 @@ tower-service = "0.3.3" urlencoding = "2.1.3" yaak-common = { workspace = true } yaak-models = { workspace = true } +yaak-templates = { workspace = true } yaak-tls = { workspace = true } diff --git a/crates/yaak-http/src/types.rs b/crates/yaak-http/src/types.rs index 3ca14e15..b1e6e655 100644 --- a/crates/yaak-http/src/types.rs +++ b/crates/yaak-http/src/types.rs @@ -9,8 +9,9 @@ use std::collections::BTreeMap; use std::pin::Pin; use std::time::Duration; use tokio::io::AsyncRead; -use yaak_common::serde::{get_bool, get_str, get_str_map}; +use yaak_common::serde::{get_bool, get_bool_map, get_str, get_str_map}; use yaak_models::models::HttpRequest; +use yaak_templates::strip_json_comments::{maybe_strip_json_comments, strip_json_comments}; pub(crate) const MULTIPART_BOUNDARY: &str = "------YaakFormBoundary"; @@ -134,16 +135,69 @@ pub fn append_query_params(url: &str, params: Vec<(String, String)>) -> String { result } +fn strip_query_params(url: &str, names: &[&str]) -> String { + // Split off fragment + let (base_and_query, fragment) = if let Some(hash_pos) = url.find('#') { + (&url[..hash_pos], Some(&url[hash_pos..])) + } else { + (url, None) + }; + + let result = if let Some(q_pos) = base_and_query.find('?') { + let base = &base_and_query[..q_pos]; + let query = &base_and_query[q_pos + 1..]; + let filtered: Vec<&str> = query + .split('&') + .filter(|pair| { + let key = pair.split('=').next().unwrap_or(""); + let decoded = urlencoding::decode(key).unwrap_or_default(); + !names.contains(&decoded.as_ref()) + }) + .collect(); + if filtered.is_empty() { + base.to_string() + } else { + format!("{}?{}", base, filtered.join("&")) + } + } else { + base_and_query.to_string() + }; + + match fragment { + Some(f) => format!("{}{}", result, f), + None => result, + } +} + fn build_url(r: &HttpRequest) -> String { let (url_string, params) = apply_path_placeholders(&ensure_proto(&r.url), &r.url_parameters); - append_query_params( + let mut url = append_query_params( &url_string, params .iter() .filter(|p| p.enabled && !p.name.is_empty()) .map(|p| (p.name.clone(), p.value.clone())) .collect(), - ) + ); + + // GraphQL GET requests encode query/variables as URL query parameters + if r.method.to_lowercase() == "get" && r.body_type.as_deref() == Some("graphql") { + url = append_graphql_query_params(&url, &r.body); + } + + url +} + +fn append_graphql_query_params(url: &str, body: &BTreeMap) -> String { + let query = get_str_map(body, "query").to_string(); + let variables = strip_json_comments(&get_str_map(body, "variables")); + let mut params = vec![("query".to_string(), query)]; + if !variables.trim().is_empty() { + params.push(("variables".to_string(), variables)); + } + // Strip existing query/variables params to avoid duplicates + let url = strip_query_params(url, &["query", "variables"]); + append_query_params(&url, params) } fn build_headers(r: &HttpRequest) -> Vec<(String, String)> { @@ -177,7 +231,7 @@ async fn build_body( (build_form_body(&body), Some("application/x-www-form-urlencoded".to_string())) } "multipart/form-data" => build_multipart_body(&body, &headers).await?, - _ if body.contains_key("text") => (build_text_body(&body), None), + _ if body.contains_key("text") => (build_text_body(&body, body_type), None), t => { warn!("Unsupported body type: {}", t); (None, None) @@ -252,13 +306,20 @@ async fn build_binary_body( })) } -fn build_text_body(body: &BTreeMap) -> Option { +fn build_text_body(body: &BTreeMap, body_type: &str) -> Option { let text = get_str_map(body, "text"); if text.is_empty() { - None - } else { - Some(SendableBodyWithMeta::Bytes(Bytes::from(text.to_string()))) + return None; } + + let send_comments = get_bool_map(body, "sendJsonComments", false); + let text = if !send_comments && body_type == "application/json" { + maybe_strip_json_comments(text) + } else { + text.to_string() + }; + + Some(SendableBodyWithMeta::Bytes(Bytes::from(text))) } fn build_graphql_body( @@ -266,7 +327,7 @@ fn build_graphql_body( body: &BTreeMap, ) -> Option { let query = get_str_map(body, "query"); - let variables = get_str_map(body, "variables"); + let variables = strip_json_comments(&get_str_map(body, "variables")); if method.to_lowercase() == "get" { // GraphQL GET requests use query parameters, not a body @@ -684,7 +745,7 @@ mod tests { let mut body = BTreeMap::new(); body.insert("text".to_string(), json!("Hello, World!")); - let result = build_text_body(&body); + let result = build_text_body(&body, "application/json"); match result { Some(SendableBodyWithMeta::Bytes(bytes)) => { assert_eq!(bytes, Bytes::from("Hello, World!")) @@ -698,7 +759,7 @@ mod tests { let mut body = BTreeMap::new(); body.insert("text".to_string(), json!("")); - let result = build_text_body(&body); + let result = build_text_body(&body, "application/json"); assert!(result.is_none()); } @@ -706,10 +767,57 @@ mod tests { async fn test_text_body_missing() { let body = BTreeMap::new(); - let result = build_text_body(&body); + let result = build_text_body(&body, "application/json"); assert!(result.is_none()); } + #[tokio::test] + async fn test_text_body_strips_json_comments_by_default() { + let mut body = BTreeMap::new(); + body.insert("text".to_string(), json!("{\n // comment\n \"foo\": \"bar\"\n}")); + + let result = build_text_body(&body, "application/json"); + match result { + Some(SendableBodyWithMeta::Bytes(bytes)) => { + let text = String::from_utf8_lossy(&bytes); + assert!(!text.contains("// comment")); + assert!(text.contains("\"foo\": \"bar\"")); + } + _ => panic!("Expected Some(SendableBody::Bytes)"), + } + } + + #[tokio::test] + async fn test_text_body_send_json_comments_when_opted_in() { + let mut body = BTreeMap::new(); + body.insert("text".to_string(), json!("{\n // comment\n \"foo\": \"bar\"\n}")); + body.insert("sendJsonComments".to_string(), json!(true)); + + let result = build_text_body(&body, "application/json"); + match result { + Some(SendableBodyWithMeta::Bytes(bytes)) => { + let text = String::from_utf8_lossy(&bytes); + assert!(text.contains("// comment")); + } + _ => panic!("Expected Some(SendableBody::Bytes)"), + } + } + + #[tokio::test] + async fn test_text_body_no_strip_for_non_json() { + let mut body = BTreeMap::new(); + body.insert("text".to_string(), json!("// not json\nsome text")); + + let result = build_text_body(&body, "text/plain"); + match result { + Some(SendableBodyWithMeta::Bytes(bytes)) => { + let text = String::from_utf8_lossy(&bytes); + assert!(text.contains("// not json")); + } + _ => panic!("Expected Some(SendableBody::Bytes)"), + } + } + #[tokio::test] async fn test_form_urlencoded_body() -> Result<()> { let mut body = BTreeMap::new(); diff --git a/crates/yaak-templates/src/format_json.rs b/crates/yaak-templates/src/format_json.rs index 1a7b5cd4..98f1a90e 100644 --- a/crates/yaak-templates/src/format_json.rs +++ b/crates/yaak-templates/src/format_json.rs @@ -11,6 +11,7 @@ pub fn format_json(text: &str, tab: &str) -> String { let mut new_json = "".to_string(); let mut depth = 0; let mut state = FormatState::None; + let mut saw_newline_in_whitespace = false; loop { let rest_of_chars = chars.clone(); @@ -61,6 +62,62 @@ pub fn format_json(text: &str, tab: &str) -> String { continue; } + // Handle line comments (//) + if current_char == '/' && chars.peek() == Some(&'/') { + chars.next(); // Skip second / + // Collect the rest of the comment until newline + let mut comment = String::from("//"); + loop { + match chars.peek() { + Some(&'\n') | None => break, + Some(_) => comment.push(chars.next().unwrap()), + } + } + // Check if the comma handler already added \n + indent + let trimmed = new_json.trim_end_matches(|c: char| c == ' ' || c == '\t'); + if trimmed.ends_with(",\n") && !saw_newline_in_whitespace { + // Trailing comment on the same line as comma (e.g. "foo",// comment) + new_json.truncate(trimmed.len() - 1); + new_json.push(' '); + } else if !trimmed.ends_with('\n') && !new_json.is_empty() { + // Trailing comment after a value (no newline before us) + new_json.push(' '); + } + new_json.push_str(&comment); + new_json.push('\n'); + new_json.push_str(tab.to_string().repeat(depth).as_str()); + saw_newline_in_whitespace = false; + continue; + } + + // Handle block comments (/* ... */) + if current_char == '/' && chars.peek() == Some(&'*') { + chars.next(); // Skip * + let mut comment = String::from("/*"); + loop { + match chars.next() { + None => break, + Some('*') if chars.peek() == Some(&'/') => { + chars.next(); // Skip / + comment.push_str("*/"); + break; + } + Some(c) => comment.push(c), + } + } + // If we're not already on a fresh line, add newline + indent before comment + let trimmed = new_json.trim_end_matches(|c: char| c == ' ' || c == '\t'); + if !trimmed.is_empty() && !trimmed.ends_with('\n') { + new_json.push('\n'); + new_json.push_str(tab.to_string().repeat(depth).as_str()); + } + new_json.push_str(&comment); + // After block comment, add newline + indent for the next content + new_json.push('\n'); + new_json.push_str(tab.to_string().repeat(depth).as_str()); + continue; + } + match current_char { ',' => { new_json.push(current_char); @@ -125,20 +182,37 @@ pub fn format_json(text: &str, tab: &str) -> String { || current_char == '\t' || current_char == '\r' { + if current_char == '\n' { + saw_newline_in_whitespace = true; + } // Don't add these } else { + saw_newline_in_whitespace = false; new_json.push(current_char); } } } } - // Replace only lines containing whitespace with nothing - new_json - .lines() - .filter(|line| !line.trim().is_empty()) // Filter out whitespace-only lines - .collect::>() // Collect the non-empty lines into a vector - .join("\n") // Join the lines back into a single string + // Filter out whitespace-only lines, but preserve empty lines inside block comments + let mut result_lines: Vec<&str> = Vec::new(); + let mut in_block_comment = false; + for line in new_json.lines() { + if in_block_comment { + result_lines.push(line); + if line.contains("*/") { + in_block_comment = false; + } + } else { + if line.contains("/*") && !line.contains("*/") { + in_block_comment = true; + } + if !line.trim().is_empty() { + result_lines.push(line); + } + } + } + result_lines.iter().map(|line| line.trim_end()).collect::>().join("\n") } #[cfg(test)] @@ -297,6 +371,161 @@ mod tests { r#" {} } +"# + .trim() + ); + } + + #[test] + fn test_line_comment_between_keys() { + assert_eq!( + format_json( + r#"{"foo":"bar",// a comment +"baz":"qux"}"#, + " " + ), + r#" +{ + "foo": "bar", // a comment + "baz": "qux" +} +"# + .trim() + ); + } + + #[test] + fn test_line_comment_at_end() { + assert_eq!( + format_json( + r#"{"foo":"bar" // trailing +}"#, + " " + ), + r#" +{ + "foo": "bar" // trailing +} +"# + .trim() + ); + } + + #[test] + fn test_block_comment() { + assert_eq!( + format_json(r#"{"foo":"bar",/* comment */"baz":"qux"}"#, " "), + r#" +{ + "foo": "bar", + /* comment */ + "baz": "qux" +} +"# + .trim() + ); + } + + #[test] + fn test_comment_in_array() { + assert_eq!( + format_json( + r#"[1,// item comment +2,3]"#, + " " + ), + r#" +[ + 1, // item comment + 2, + 3 +] +"# + .trim() + ); + } + + #[test] + fn test_comment_only_line() { + assert_eq!( + format_json( + r#"{ + // this is a standalone comment + "foo": "bar" +}"#, + " " + ), + r#" +{ + // this is a standalone comment + "foo": "bar" +} +"# + .trim() + ); + } + + #[test] + fn test_multiline_block_comment() { + assert_eq!( + format_json( + r#"{ + "foo": "bar" + /** + Hello World! + + Hi there + */ +}"#, + " " + ), + r#" +{ + "foo": "bar" + /** + Hello World! + + Hi there + */ +} +"# + .trim() + ); + } + + // NOTE: trailing whitespace on output lines is trimmed by the formatter. + // We can't easily add a test for this because raw string literals get + // trailing whitespace stripped by the editor/linter. + + #[test] + fn test_comment_inside_string_ignored() { + assert_eq!( + format_json(r#"{"foo":"// not a comment","bar":"/* also not */"}"#, " "), + r#" +{ + "foo": "// not a comment", + "bar": "/* also not */" +} +"# + .trim() + ); + } + + #[test] + fn test_comment_on_line_after_comma() { + assert_eq!( + format_json( + r#"{ + "a": "aaa", + // "b": "bbb" +}"#, + " " + ), + r#" +{ + "a": "aaa", + // "b": "bbb" +} "# .trim() ); diff --git a/crates/yaak-templates/src/lib.rs b/crates/yaak-templates/src/lib.rs index 93708b66..db5c4e14 100644 --- a/crates/yaak-templates/src/lib.rs +++ b/crates/yaak-templates/src/lib.rs @@ -1,6 +1,7 @@ pub mod error; pub mod escape; pub mod format_json; +pub mod strip_json_comments; pub mod parser; pub mod renderer; pub mod wasm; diff --git a/crates/yaak-templates/src/strip_json_comments.rs b/crates/yaak-templates/src/strip_json_comments.rs new file mode 100644 index 00000000..ade19c8c --- /dev/null +++ b/crates/yaak-templates/src/strip_json_comments.rs @@ -0,0 +1,318 @@ +/// Strips JSON comments only if the result is valid JSON. If stripping comments +/// produces invalid JSON, the original text is returned unchanged. +pub fn maybe_strip_json_comments(text: &str) -> String { + let stripped = strip_json_comments(text); + if serde_json::from_str::(&stripped).is_ok() { + stripped + } else { + text.to_string() + } +} + +/// Strips comments from JSONC, preserving the original formatting as much as possible. +/// +/// - Trailing comments on a line are removed (along with preceding whitespace) +/// - Whole-line comments are removed, including the line itself +/// - Block comments are removed, including any lines that become empty +/// - Comments inside strings and template tags are left alone +pub fn strip_json_comments(text: &str) -> String { + let mut chars = text.chars().peekable(); + let mut result = String::with_capacity(text.len()); + let mut in_string = false; + let mut in_template_tag = false; + + loop { + let current_char = match chars.next() { + None => break, + Some(c) => c, + }; + + // Handle JSON strings + if in_string { + result.push(current_char); + match current_char { + '"' => in_string = false, + '\\' => { + if let Some(c) = chars.next() { + result.push(c); + } + } + _ => {} + } + continue; + } + + // Handle template tags + if in_template_tag { + result.push(current_char); + if current_char == ']' && chars.peek() == Some(&'}') { + result.push(chars.next().unwrap()); + in_template_tag = false; + } + continue; + } + + // Check for template tag start + if current_char == '$' && chars.peek() == Some(&'{') { + let mut lookahead = chars.clone(); + lookahead.next(); // skip { + if lookahead.peek() == Some(&'[') { + in_template_tag = true; + result.push(current_char); + result.push(chars.next().unwrap()); // { + result.push(chars.next().unwrap()); // [ + continue; + } + } + + // Check for line comment + if current_char == '/' && chars.peek() == Some(&'/') { + chars.next(); // skip second / + // Consume until newline + loop { + match chars.peek() { + Some(&'\n') | None => break, + Some(_) => { + chars.next(); + } + } + } + // Trim trailing whitespace that preceded the comment + let trimmed_len = result.trim_end_matches(|c: char| c == ' ' || c == '\t').len(); + result.truncate(trimmed_len); + continue; + } + + // Check for block comment + if current_char == '/' && chars.peek() == Some(&'*') { + chars.next(); // skip * + // Consume until */ + loop { + match chars.next() { + None => break, + Some('*') if chars.peek() == Some(&'/') => { + chars.next(); // skip / + break; + } + Some(_) => {} + } + } + // Trim trailing whitespace that preceded the comment + let trimmed_len = result.trim_end_matches(|c: char| c == ' ' || c == '\t').len(); + result.truncate(trimmed_len); + // Skip whitespace/newline after the block comment if the next line is content + // (this handles the case where the block comment is on its own line) + continue; + } + + if current_char == '"' { + in_string = true; + } + + result.push(current_char); + } + + // Remove lines that are now empty (were comment-only lines) + let result = result + .lines() + .filter(|line| !line.trim().is_empty()) + .collect::>() + .join("\n"); + + // Remove trailing commas before } or ] + strip_trailing_commas(&result) +} + +/// Removes trailing commas before closing braces/brackets, respecting strings. +fn strip_trailing_commas(text: &str) -> String { + let mut result = String::with_capacity(text.len()); + let chars: Vec = text.chars().collect(); + let mut i = 0; + let mut in_string = false; + + while i < chars.len() { + let ch = chars[i]; + + if in_string { + result.push(ch); + match ch { + '"' => in_string = false, + '\\' => { + i += 1; + if i < chars.len() { + result.push(chars[i]); + } + } + _ => {} + } + i += 1; + continue; + } + + if ch == '"' { + in_string = true; + result.push(ch); + i += 1; + continue; + } + + if ch == ',' { + // Look ahead past whitespace/newlines for } or ] + let mut j = i + 1; + while j < chars.len() && chars[j].is_whitespace() { + j += 1; + } + if j < chars.len() && (chars[j] == '}' || chars[j] == ']') { + // Skip the comma + i += 1; + continue; + } + } + + result.push(ch); + i += 1; + } + + result +} + +#[cfg(test)] +mod tests { + use crate::strip_json_comments::strip_json_comments; + + #[test] + fn test_no_comments() { + let input = r#"{ + "foo": "bar", + "baz": 123 +}"#; + assert_eq!(strip_json_comments(input), input); + } + + #[test] + fn test_trailing_line_comment() { + assert_eq!( + strip_json_comments(r#"{ + "foo": "bar", // this is a comment + "baz": 123 +}"#), + r#"{ + "foo": "bar", + "baz": 123 +}"# + ); + } + + #[test] + fn test_whole_line_comment() { + assert_eq!( + strip_json_comments(r#"{ + // this is a comment + "foo": "bar" +}"#), + r#"{ + "foo": "bar" +}"# + ); + } + + #[test] + fn test_inline_block_comment() { + assert_eq!( + strip_json_comments(r#"{ + "foo": /* a comment */ "bar" +}"#), + r#"{ + "foo": "bar" +}"# + ); + } + + #[test] + fn test_whole_line_block_comment() { + assert_eq!( + strip_json_comments(r#"{ + /* a comment */ + "foo": "bar" +}"#), + r#"{ + "foo": "bar" +}"# + ); + } + + #[test] + fn test_multiline_block_comment() { + assert_eq!( + strip_json_comments(r#"{ + /** + * Hello World! + */ + "foo": "bar" +}"#), + r#"{ + "foo": "bar" +}"# + ); + } + + #[test] + fn test_comment_inside_string_preserved() { + let input = r#"{ + "foo": "// not a comment", + "bar": "/* also not */" +}"#; + assert_eq!(strip_json_comments(input), input); + } + + #[test] + fn test_comment_inside_template_tag_preserved() { + let input = r#"{ + "foo": ${[ fn("// hi", "/* hey */") ]} +}"#; + assert_eq!(strip_json_comments(input), input); + } + + #[test] + fn test_multiple_comments() { + assert_eq!( + strip_json_comments(r#"{ + // first comment + "foo": "bar", // trailing + /* block */ + "baz": 123 +}"#), + r#"{ + "foo": "bar", + "baz": 123 +}"# + ); + } + + #[test] + fn test_trailing_comma_after_comment_removed() { + assert_eq!( + strip_json_comments(r#"{ + "a": "aaa", + // "b": "bbb" +}"#), + r#"{ + "a": "aaa" +}"# + ); + } + + #[test] + fn test_trailing_comma_in_array() { + assert_eq!( + strip_json_comments(r#"[1, 2, /* 3 */]"#), + r#"[1, 2]"# + ); + } + + #[test] + fn test_comma_inside_string_preserved() { + let input = r#"{"a": "hello,}"#; + assert_eq!(strip_json_comments(input), input); + } +} diff --git a/crates/yaak/src/render.rs b/crates/yaak/src/render.rs index 64b5e04e..75015ae7 100644 --- a/crates/yaak/src/render.rs +++ b/crates/yaak/src/render.rs @@ -2,7 +2,7 @@ use log::info; use serde_json::Value; use std::collections::BTreeMap; use yaak_http::path_placeholders::apply_path_placeholders; -use yaak_models::models::{Environment, HttpRequest, HttpRequestHeader, HttpUrlParameter}; +use yaak_models::models::{Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter}; use yaak_models::render::make_vars_hashmap; use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw}; @@ -89,6 +89,64 @@ pub async fn render_http_request( Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..request.to_owned() }) } +pub async fn render_grpc_request( + r: &GrpcRequest, + environment_chain: Vec, + cb: &T, + opt: &RenderOptions, +) -> yaak_templates::error::Result { + let vars = &make_vars_hashmap(environment_chain); + + let mut metadata = Vec::new(); + for p in r.metadata.clone() { + if !p.enabled { + continue; + } + metadata.push(HttpRequestHeader { + enabled: p.enabled, + name: parse_and_render(p.name.as_str(), vars, cb, opt).await?, + value: parse_and_render(p.value.as_str(), vars, cb, opt).await?, + id: p.id, + }) + } + + let authentication = { + let mut disabled = false; + let mut auth = BTreeMap::new(); + match r.authentication.get("disabled") { + Some(Value::Bool(true)) => { + disabled = true; + } + Some(Value::String(tmpl)) => { + disabled = parse_and_render(tmpl.as_str(), vars, cb, opt) + .await + .unwrap_or_default() + .is_empty(); + info!( + "Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\"" + ); + } + _ => {} + } + if disabled { + auth.insert("disabled".to_string(), Value::Bool(true)); + } else { + for (k, v) in r.authentication.clone() { + if k == "disabled" { + auth.insert(k, Value::Bool(false)); + } else { + auth.insert(k, render_json_value_raw(v, vars, cb, opt).await?); + } + } + } + auth + }; + + let url = parse_and_render(r.url.as_str(), vars, cb, opt).await?; + + Ok(GrpcRequest { url, metadata, authentication, ..r.to_owned() }) +} + fn strip_disabled_form_entries(v: Value) -> Value { match v { Value::Array(items) => Value::Array( diff --git a/package-lock.json b/package-lock.json index 27715ed7..938c3f65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2584,6 +2584,16 @@ "ebnf": "^1.9.1" } }, + "node_modules/@shopify/lang-jsonc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shopify/lang-jsonc/-/lang-jsonc-1.0.1.tgz", + "integrity": "sha512-KrBrRFhvr1qJiZBODAtqbL1u1e67UR3plBN79Z8nd5TQAAzpx66jS4zs7Ss9M22ygGrpWFhyhSoNVlp5VCYktQ==", + "license": "MIT", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.7" + } + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -16323,6 +16333,7 @@ "@replit/codemirror-emacs": "^6.1.0", "@replit/codemirror-vim": "^6.3.0", "@replit/codemirror-vscode-keymap": "^6.0.2", + "@shopify/lang-jsonc": "^1.0.1", "@tanstack/react-query": "^5.90.5", "@tanstack/react-router": "^1.133.13", "@tanstack/react-virtual": "^3.13.12", diff --git a/src-web/components/GrpcEditor.tsx b/src-web/components/GrpcEditor.tsx index 4a98582a..6727e8f6 100644 --- a/src-web/components/GrpcEditor.tsx +++ b/src-web/components/GrpcEditor.tsx @@ -1,4 +1,4 @@ -import { jsonLanguage } from '@codemirror/lang-json'; +import { jsoncLanguage } from '@shopify/lang-jsonc'; import { linter } from '@codemirror/lint'; import type { EditorView } from '@codemirror/view'; import type { GrpcRequest } from '@yaakapp-internal/models'; @@ -115,7 +115,7 @@ export function GrpcEditor({ delay: 200, needsRefresh: handleRefresh, }), - jsonLanguage.data.of({ + jsoncLanguage.data.of({ autocomplete: jsonCompletion(), }), stateExtensions({}), diff --git a/src-web/components/HttpRequestPane.tsx b/src-web/components/HttpRequestPane.tsx index 70300900..75f857b3 100644 --- a/src-web/components/HttpRequestPane.tsx +++ b/src-web/components/HttpRequestPane.tsx @@ -48,6 +48,7 @@ import { FormMultipartEditor } from './FormMultipartEditor'; import { FormUrlencodedEditor } from './FormUrlencodedEditor'; import { HeadersEditor } from './HeadersEditor'; import { HttpAuthenticationEditor } from './HttpAuthenticationEditor'; +import { JsonBodyEditor } from './JsonBodyEditor'; import { MarkdownEditor } from './MarkdownEditor'; import { RequestMethodDropdown } from './RequestMethodDropdown'; import { UrlBar } from './UrlBar'; @@ -257,7 +258,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }: ); const handleBodyTextChange = useCallback( - (text: string) => patchModel(activeRequest, { body: { text } }), + (text: string) => patchModel(activeRequest, { body: { ...activeRequest.body, text } }), [activeRequest], ); @@ -370,16 +371,10 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }: {activeRequest.bodyType === BODY_TYPE_JSON ? ( - ) : activeRequest.bodyType === BODY_TYPE_XML ? ( patchModel(request, { body: { ...request.body, text } }), + [request], + ); + + const autoFix = request.body?.sendJsonComments !== true; + + const lintExtension = useMemo( + () => + linter( + jsonParseLinter( + autoFix + ? { allowComments: true, allowTrailingCommas: true } + : { allowComments: false, allowTrailingCommas: false }, + ), + ), + [autoFix], + ); + + const hasComments = useMemo( + () => textLikelyContainsJsonComments(request.body?.text ?? ''), + [request.body?.text], + ); + + const { value: bannerDismissed, set: setBannerDismissed } = useKeyValue({ + namespace: 'no_sync', + key: ['json-fix-3', request.workspaceId], + fallback: false, + }); + + const handleToggleAutoFix = useCallback(() => { + const newBody = { ...request.body }; + if (autoFix) { + newBody.sendJsonComments = true; + } else { + delete newBody.sendJsonComments; + } + patchModel(request, { body: newBody }); + }, [request, autoFix]); + + const handleDropdownOpen = useCallback(() => { + if (!bannerDismissed) { + setBannerDismissed(true); + } + }, [bannerDismissed, setBannerDismissed]); + + const showBanner = hasComments && autoFix && !bannerDismissed; + + const stripMessage = 'Automatically strip comments and trailing commas before sending'; + const actions = useMemo( + () => [ + showBanner && ( + +

+ Auto-fix enabled + +

+
+ ), +
+ , + leftSlot: ( + + ), + }, + ] satisfies DropdownItem[] + } + > + + +
, + ], + [handleDropdownOpen, handleToggleAutoFix, autoFix, showBanner], + ); + + return ( + + ); +} diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 829211fe..f5d45794 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -78,6 +78,7 @@ export interface EditorProps { hideGutter?: boolean; id?: string; language?: EditorLanguage | 'pairs' | 'url' | 'timeline' | null; + lintExtension?: Extension; graphQLSchema?: GraphQLSchema | null; onBlur?: () => void; onChange?: (value: string) => void; @@ -124,6 +125,7 @@ function EditorInner({ hideGutter, graphQLSchema, language, + lintExtension, onBlur, onChange, onFocus, @@ -332,6 +334,7 @@ function EditorInner({ const ext = getLanguageExtension({ useTemplating, language, + lintExtension, hideGutter, environmentVariables, autocomplete, @@ -344,6 +347,7 @@ function EditorInner({ view.dispatch({ effects: languageCompartment.reconfigure(ext) }); }, [ language, + lintExtension, autocomplete, environmentVariables, onClickFunction, @@ -371,6 +375,7 @@ function EditorInner({ const langExt = getLanguageExtension({ useTemplating, language, + lintExtension, completionOptions, autocomplete, environmentVariables, diff --git a/src-web/components/core/Editor/extensions.ts b/src-web/components/core/Editor/extensions.ts index 1ec1eb07..8db6750d 100644 --- a/src-web/components/core/Editor/extensions.ts +++ b/src-web/components/core/Editor/extensions.ts @@ -8,7 +8,6 @@ import { history, historyKeymap } from '@codemirror/commands'; import { go } from '@codemirror/lang-go'; import { java } from '@codemirror/lang-java'; import { javascript } from '@codemirror/lang-javascript'; -import { json } from '@codemirror/lang-json'; import { markdown } from '@codemirror/lang-markdown'; import { php } from '@codemirror/lang-php'; import { python } from '@codemirror/lang-python'; @@ -34,7 +33,6 @@ import { ruby } from '@codemirror/legacy-modes/mode/ruby'; import { shell } from '@codemirror/legacy-modes/mode/shell'; import { swift } from '@codemirror/legacy-modes/mode/swift'; import { linter, lintGutter, lintKeymap } from '@codemirror/lint'; - import { search, searchKeymap } from '@codemirror/search'; import type { Extension } from '@codemirror/state'; import { EditorState } from '@codemirror/state'; @@ -50,6 +48,7 @@ import { rectangularSelection, } from '@codemirror/view'; import { tags as t } from '@lezer/highlight'; +import { jsonc, jsoncLanguage } from '@shopify/lang-jsonc'; import { graphql } from 'cm6-graphql'; import type { GraphQLSchema } from 'graphql'; import { activeRequestIdAtom } from '../../../hooks/useActiveRequestId'; @@ -61,13 +60,13 @@ import { showGraphQLDocExplorerAtom } from '../../graphql/graphqlAtoms'; import type { EditorProps } from './Editor'; import { jsonParseLinter } from './json-lint'; import { pairs } from './pairs/extension'; +import { searchMatchCount } from './searchMatchCount'; import { text } from './text/extension'; import { timeline } from './timeline/extension'; import type { TwigCompletionOption } from './twig/completion'; import { twig } from './twig/extension'; import { pathParametersPlugin } from './twig/pathParameters'; import { url } from './url/extension'; -import { searchMatchCount } from './searchMatchCount'; export const syntaxHighlightStyle = HighlightStyle.define([ { @@ -107,7 +106,7 @@ const syntaxExtensions: Record< null | (() => LanguageSupport) > = { graphql: null, - json: json, + json: jsonc, javascript: javascript, // HTML as XML because HTML is oddly slow html: xml, @@ -140,6 +139,7 @@ const closeBracketsFor: (keyof typeof syntaxExtensions)[] = ['json', 'javascript export function getLanguageExtension({ useTemplating, language = 'text', + lintExtension, environmentVariables, autocomplete, hideGutter, @@ -156,7 +156,7 @@ export function getLanguageExtension({ onClickPathParameter: (name: string) => void; completionOptions: TwigCompletionOption[]; graphQLSchema: GraphQLSchema | null; -} & Pick) { +} & Pick) { const extraExtensions: Extension[] = []; if (language === 'url') { @@ -193,7 +193,12 @@ export function getLanguageExtension({ } if (language === 'json') { - extraExtensions.push(linter(jsonParseLinter())); + extraExtensions.push(lintExtension ?? linter(jsonParseLinter())); + extraExtensions.push( + jsoncLanguage.data.of({ + commentTokens: { line: '//', block: { open: '/*', close: '*/' } }, + }), + ); if (!hideGutter) { extraExtensions.push(lintGutter()); } diff --git a/src-web/components/core/Editor/json-lint.ts b/src-web/components/core/Editor/json-lint.ts index f4b080dd..32652f15 100644 --- a/src-web/components/core/Editor/json-lint.ts +++ b/src-web/components/core/Editor/json-lint.ts @@ -4,14 +4,22 @@ import { parse as jsonLintParse } from '@prantlf/jsonlint'; const TEMPLATE_SYNTAX_REGEX = /\$\{\[[\s\S]*?]}/g; -export function jsonParseLinter() { +interface JsonLintOptions { + allowComments?: boolean; + allowTrailingCommas?: boolean; +} + +export function jsonParseLinter(options?: JsonLintOptions) { return (view: EditorView): Diagnostic[] => { try { const doc = view.state.doc.toString(); // We need lint to not break on stuff like {"foo:" ${[ ... ]}} so we'll replace all template // syntax with repeating `1` characters, so it's valid JSON and the position is still correct. const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, (m) => '1'.repeat(m.length)); - jsonLintParse(escapedDoc); + jsonLintParse(escapedDoc, { + mode: (options?.allowComments ?? true) ? 'cjson' : 'json', + ignoreTrailingCommas: options?.allowTrailingCommas ?? false, + }); // biome-ignore lint/suspicious/noExplicitAny: none } catch (err: any) { if (!('location' in err)) { diff --git a/src-web/components/graphql/GraphQLEditor.tsx b/src-web/components/graphql/GraphQLEditor.tsx index 11b3c879..1db31b0a 100644 --- a/src-web/components/graphql/GraphQLEditor.tsx +++ b/src-web/components/graphql/GraphQLEditor.tsx @@ -156,6 +156,7 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp { type: 'separator', label: 'Setting' }, { label: 'Automatic Introspection', + keepOpenOnSelect: true, onSelect: () => { setAutoIntrospectDisabled({ ...autoIntrospectDisabled, diff --git a/src-web/lib/jsonComments.ts b/src-web/lib/jsonComments.ts new file mode 100644 index 00000000..3c308955 --- /dev/null +++ b/src-web/lib/jsonComments.ts @@ -0,0 +1,30 @@ +/** + * Simple heuristic to detect if a string likely contains JSON/JSONC comments. + * Checks for // and /* patterns that are NOT inside double-quoted strings. + * Used for UI hints only — doesn't need to be perfect. + */ +export function textLikelyContainsJsonComments(text: string): boolean { + let inString = false; + for (let i = 0; i < text.length; i++) { + const ch = text[i]; + if (inString) { + if (ch === '"') { + inString = false; + } else if (ch === '\\') { + i++; // skip escaped char + } + continue; + } + if (ch === '"') { + inString = true; + continue; + } + if (ch === '/' && i + 1 < text.length) { + const next = text[i + 1]; + if (next === '/' || next === '*') { + return true; + } + } + } + return false; +} diff --git a/src-web/package.json b/src-web/package.json index 3f360cbc..45d42a59 100644 --- a/src-web/package.json +++ b/src-web/package.json @@ -27,6 +27,7 @@ "@replit/codemirror-emacs": "^6.1.0", "@replit/codemirror-vim": "^6.3.0", "@replit/codemirror-vscode-keymap": "^6.0.2", + "@shopify/lang-jsonc": "^1.0.1", "@tanstack/react-query": "^5.90.5", "@tanstack/react-router": "^1.133.13", "@tanstack/react-virtual": "^3.13.12", From cfab62707e5ad319a994a97cae404af2976fd348 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 5 Mar 2026 15:36:09 -0800 Subject: [PATCH 05/10] Exclude yaak-cli from app release tests and remove stale lint comments Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release-app.yml | 2 +- src-web/components/core/Editor/Editor.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/release-app.yml b/.github/workflows/release-app.yml index c3432afb..dc953481 100644 --- a/.github/workflows/release-app.yml +++ b/.github/workflows/release-app.yml @@ -95,7 +95,7 @@ jobs: - name: Run JS Tests run: npm test - name: Run Rust Tests - run: cargo test --all + run: cargo test --all --exclude yaak-cli - name: Set version run: npm run replace-version diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index f5d45794..a33cb7bd 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -327,7 +327,6 @@ function EditorInner({ ); // Update the language extension when the language changes - // biome-ignore lint/correctness/useExhaustiveDependencies: none useEffect(() => { if (cm.current === null) return; const { view, languageCompartment } = cm.current; @@ -361,7 +360,6 @@ function EditorInner({ ]); // Initialize the editor when ref mounts - // biome-ignore lint/correctness/useExhaustiveDependencies: Only reinitialize when necessary const initEditorRef = useCallback( function initEditorRef(container: HTMLDivElement | null) { if (container === null) { From a0cb7f813f46429b0f7d7bfdfb9490992a469622 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 5 Mar 2026 15:53:13 -0800 Subject: [PATCH 06/10] Replace format-graphql with pretty_graphql for comment-preserving GraphQL formatting Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 116 +++++-- crates-tauri/yaak-app/Cargo.toml | 1 + crates-tauri/yaak-app/src/lib.rs | 9 + package-lock.json | 312 ------------------- src-web/components/graphql/GraphQLEditor.tsx | 4 +- src-web/lib/formatters.ts | 12 + src-web/lib/tauri.ts | 1 + src-web/modules.d.ts | 1 - src-web/package.json | 1 - 9 files changed, 113 insertions(+), 344 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f822e0d0..9ea47a34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,6 +173,17 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "apollo-parser" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947e21ff51879f8a40d7519dfe619268de2afba4042a8a43878276de3cb910f0" +dependencies = [ + "memchr", + "rowan", + "thiserror 2.0.17", +] + [[package]] name = "append-only-vec" version = "0.1.8" @@ -1347,6 +1358,12 @@ dependencies = [ "libc", ] +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + [[package]] name = "cow-utils" version = "0.1.3" @@ -4556,7 +4573,7 @@ checksum = "75b1853bc34cadaa90aa09f95713d8b77ec0c0d3e2d90ccf7a74216f40d20850" dependencies = [ "flate2", "postcard", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "thiserror 2.0.17", @@ -4599,7 +4616,7 @@ dependencies = [ "hashbrown 0.16.1", "oxc_data_structures", "oxc_estree", - "rustc-hash", + "rustc-hash 2.1.1", "serde", ] @@ -4655,7 +4672,7 @@ dependencies = [ "oxc_index", "oxc_syntax", "petgraph 0.8.3", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -4676,7 +4693,7 @@ dependencies = [ "oxc_sourcemap", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -4688,7 +4705,7 @@ dependencies = [ "cow-utils", "oxc-browserslist", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", "serde", ] @@ -4763,7 +4780,7 @@ dependencies = [ "oxc_ecmascript", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -4780,7 +4797,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -4805,7 +4822,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "oxc_traverse", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -4827,7 +4844,7 @@ dependencies = [ "oxc_regular_expression", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", "seq-macro", ] @@ -4843,7 +4860,7 @@ dependencies = [ "oxc_diagnostics", "oxc_span", "phf 0.13.1", - "rustc-hash", + "rustc-hash 2.1.1", "unicode-id-start", ] @@ -4860,7 +4877,7 @@ dependencies = [ "once_cell", "papaya", "pnp", - "rustc-hash", + "rustc-hash 2.1.1", "self_cell", "serde", "serde_json", @@ -4890,7 +4907,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "phf 0.13.1", - "rustc-hash", + "rustc-hash 2.1.1", "self_cell", ] @@ -4902,7 +4919,7 @@ checksum = "c7f89482522f3cd820817d48ee4ade5b10822060d6e5e4d419f05f6d8bd29d70" dependencies = [ "base64-simd", "json-escape-simd", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", ] @@ -4966,7 +4983,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "oxc_traverse", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "sha1", @@ -4991,7 +5008,7 @@ dependencies = [ "oxc_syntax", "oxc_transformer", "oxc_traverse", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -5009,7 +5026,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -5414,7 +5431,7 @@ dependencies = [ "nodejs-built-in-modules", "pathdiff", "radix_trie", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "thiserror 2.0.17", @@ -5533,6 +5550,18 @@ dependencies = [ "termtree", ] +[[package]] +name = "pretty_graphql" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea8c38ecedb3d28a998ea783469a78587f5f984d61226cf071f6979861e9e6a9" +dependencies = [ + "apollo-parser", + "memchr", + "rowan", + "tiny_pretty", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -5713,7 +5742,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.1", "rustls", "socket2 0.5.10", "thiserror 2.0.17", @@ -5733,7 +5762,7 @@ dependencies = [ "lru-slab", "rand 0.9.1", "ring", - "rustc-hash", + "rustc-hash 2.1.1", "rustls", "rustls-pki-types", "slab", @@ -6227,7 +6256,7 @@ dependencies = [ "rolldown_tracing", "rolldown_utils", "rolldown_watcher", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "string_wizard", @@ -6317,7 +6346,7 @@ dependencies = [ "rolldown_sourcemap", "rolldown_std_utils", "rolldown_utils", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "simdutf8", @@ -6335,7 +6364,7 @@ dependencies = [ "blake3", "dashmap", "rolldown_debug_action", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "tracing", @@ -6392,7 +6421,7 @@ dependencies = [ "rolldown-ariadne", "rolldown_utils", "ropey", - "rustc-hash", + "rustc-hash 2.1.1", "sugar_path", ] @@ -6426,7 +6455,7 @@ dependencies = [ "rolldown_resolver", "rolldown_sourcemap", "rolldown_utils", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "string_wizard", @@ -6446,7 +6475,7 @@ dependencies = [ "rolldown_common", "rolldown_plugin", "rolldown_utils", - "rustc-hash", + "rustc-hash 2.1.1", "serde_json", "xxhash-rust", ] @@ -6517,7 +6546,7 @@ dependencies = [ "oxc", "oxc_sourcemap", "rolldown_utils", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -6569,7 +6598,7 @@ dependencies = [ "regex 1.11.1", "regress", "rolldown_std_utils", - "rustc-hash", + "rustc-hash 2.1.1", "serde_json", "simdutf8", "sugar_path", @@ -6599,6 +6628,18 @@ dependencies = [ "str_indices", ] +[[package]] +name = "rowan" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash 1.1.0", + "text-size", +] + [[package]] name = "rusqlite" version = "0.32.1" @@ -6641,6 +6682,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -7453,7 +7500,7 @@ dependencies = [ "memchr", "oxc_index", "oxc_sourcemap", - "rustc-hash", + "rustc-hash 2.1.1", "serde", ] @@ -8154,6 +8201,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + [[package]] name = "textwrap" version = "0.16.2" @@ -8276,6 +8329,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tiny_pretty" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650d82e943da333637be9f1567d33d605e76810a26464edfd7ae74f7ef181e95" + [[package]] name = "tinystr" version = "0.8.1" @@ -10198,6 +10257,7 @@ dependencies = [ "md5 0.8.0", "mime_guess", "openssl-sys", + "pretty_graphql", "r2d2", "r2d2_sqlite", "rand 0.9.1", diff --git a/crates-tauri/yaak-app/Cargo.toml b/crates-tauri/yaak-app/Cargo.toml index cf1e9113..06afbbc0 100644 --- a/crates-tauri/yaak-app/Cargo.toml +++ b/crates-tauri/yaak-app/Cargo.toml @@ -30,6 +30,7 @@ eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client http = { version = "1.2.0", default-features = false } log = { workspace = true } md5 = "0.8.0" +pretty_graphql = "0.2" r2d2 = "0.8.10" r2d2_sqlite = "0.25.0" mime_guess = "2.0.5" diff --git a/crates-tauri/yaak-app/src/lib.rs b/crates-tauri/yaak-app/src/lib.rs index 23d357a1..23da70a3 100644 --- a/crates-tauri/yaak-app/src/lib.rs +++ b/crates-tauri/yaak-app/src/lib.rs @@ -872,6 +872,14 @@ async fn cmd_format_json(text: &str) -> YaakResult { Ok(format_json(text, " ")) } +#[tauri::command] +async fn cmd_format_graphql(text: &str) -> YaakResult { + match pretty_graphql::format_text(text, &Default::default()) { + Ok(formatted) => Ok(formatted), + Err(_) => Ok(text.to_string()), + } +} + #[tauri::command] async fn cmd_http_response_body( window: WebviewWindow, @@ -1641,6 +1649,7 @@ pub fn run() { 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, diff --git a/package-lock.json b/package-lock.json index 938c3f65..61ec4464 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5340,105 +5340,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/cliui/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/clone-regexp": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz", @@ -6057,15 +5958,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -7210,19 +7102,6 @@ "dev": true, "license": "MIT" }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/focus-trap": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.7.1.tgz", @@ -7315,30 +7194,6 @@ "node": ">=0.4.x" } }, - "node_modules/format-graphql": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/format-graphql/-/format-graphql-1.5.0.tgz", - "integrity": "sha512-ZZWcjwJ1IMdnW9l3CeYccC/J7skqOB18tY3autO5OUQuGVZpQu6Es3SThRm25SfiMeZO1+UbzIqnGbjAURu/UA==", - "dependencies": { - "graphql": "^15.1.0", - "yargs": "^15.3.1" - }, - "bin": { - "format-graphql": "dist/bin/index.js" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/format-graphql/node_modules/graphql": { - "version": "15.10.1", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.10.1.tgz", - "integrity": "sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==", - "license": "MIT", - "engines": { - "node": ">= 10.x" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -9281,18 +9136,6 @@ "node": ">=4" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -11615,33 +11458,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-map": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", @@ -11668,15 +11484,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -11770,15 +11577,6 @@ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "license": "MIT" }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -12893,12 +12691,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC" - }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", @@ -13366,12 +13158,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -15611,12 +15397,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC" - }, "node_modules/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", @@ -15832,12 +15612,6 @@ "node": ">=0.4" } }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -15860,91 +15634,6 @@ "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-parser/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", @@ -16353,7 +16042,6 @@ "deep-equal": "^2.2.3", "eventemitter3": "^5.0.1", "focus-trap-react": "^11.0.4", - "format-graphql": "^1.5.0", "fuzzbunny": "^1.0.1", "hexy": "^0.3.5", "history": "^5.3.0", diff --git a/src-web/components/graphql/GraphQLEditor.tsx b/src-web/components/graphql/GraphQLEditor.tsx index 1db31b0a..693ce634 100644 --- a/src-web/components/graphql/GraphQLEditor.tsx +++ b/src-web/components/graphql/GraphQLEditor.tsx @@ -1,6 +1,5 @@ import type { HttpRequest } from '@yaakapp-internal/models'; -import { formatSdl } from 'format-graphql'; import { useAtom } from 'jotai'; import { useCallback, useMemo } from 'react'; import { useLocalStorage } from 'react-use'; @@ -16,6 +15,7 @@ import { Editor } from '../core/Editor/LazyEditor'; import { FormattedError } from '../core/FormattedError'; import { Icon } from '../core/Icon'; import { Separator } from '../core/Separator'; +import { tryFormatGraphql } from '../../lib/formatters'; import { showGraphQLDocExplorerAtom } from './graphqlAtoms'; type Props = Pick & { @@ -211,7 +211,7 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp language="graphql" heightMode="auto" graphQLSchema={schema} - format={formatSdl} + format={tryFormatGraphql} defaultValue={currentBody.query} onChange={handleChangeQuery} placeholder="..." diff --git a/src-web/lib/formatters.ts b/src-web/lib/formatters.ts index f0d9bf37..0ac6381f 100644 --- a/src-web/lib/formatters.ts +++ b/src-web/lib/formatters.ts @@ -20,6 +20,18 @@ export async function tryFormatJson(text: string): Promise { return text; } +export async function tryFormatGraphql(text: string): Promise { + if (text === '') return text; + + try { + return await invokeCmd('cmd_format_graphql', { text }); + } catch (err) { + console.warn('Failed to format GraphQL', err); + } + + return text; +} + export async function tryFormatXml(text: string): Promise { if (text === '') return text; diff --git a/src-web/lib/tauri.ts b/src-web/lib/tauri.ts index 95fa68cc..b8946752 100644 --- a/src-web/lib/tauri.ts +++ b/src-web/lib/tauri.ts @@ -17,6 +17,7 @@ type TauriCmd = | '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' diff --git a/src-web/modules.d.ts b/src-web/modules.d.ts index 29ce54f6..2dcdc452 100644 --- a/src-web/modules.d.ts +++ b/src-web/modules.d.ts @@ -1,2 +1 @@ -declare module 'format-graphql'; declare module 'vkbeautify'; diff --git a/src-web/package.json b/src-web/package.json index 45d42a59..a0129314 100644 --- a/src-web/package.json +++ b/src-web/package.json @@ -47,7 +47,6 @@ "deep-equal": "^2.2.3", "eventemitter3": "^5.0.1", "focus-trap-react": "^11.0.4", - "format-graphql": "^1.5.0", "fuzzbunny": "^1.0.1", "hexy": "^0.3.5", "history": "^5.3.0", From 591c68c59cfe6319c1459daf4cfc194ba415f78a Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 5 Mar 2026 15:59:10 -0800 Subject: [PATCH 07/10] Revert macOS CI runners back to macos-latest Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release-app.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-app.yml b/.github/workflows/release-app.yml index dc953481..0506d436 100644 --- a/.github/workflows/release-app.yml +++ b/.github/workflows/release-app.yml @@ -13,12 +13,12 @@ jobs: fail-fast: false matrix: include: - - platform: "macos-26" # for Arm-based Macs (M1 and above). + - platform: "macos-latest" # for Arm-based Macs (M1 and above). args: "--target aarch64-apple-darwin" yaak_arch: "arm64" os: "macos" targets: "aarch64-apple-darwin" - - platform: "macos-26-intel" # for Intel-based Macs. + - platform: "macos-latest" # for Intel-based Macs. args: "--target x86_64-apple-darwin" yaak_arch: "x64" os: "macos" From 3d577dd7d985fab4a24eca1651205cdefdbb6de1 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 5 Mar 2026 16:06:40 -0800 Subject: [PATCH 08/10] Update release skills for CLI --- .claude/commands/release/generate-release-notes.md | 1 + .codex/skills/release-generate-release-notes/SKILL.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.claude/commands/release/generate-release-notes.md b/.claude/commands/release/generate-release-notes.md index 6a42ea32..97fba5bc 100644 --- a/.claude/commands/release/generate-release-notes.md +++ b/.claude/commands/release/generate-release-notes.md @@ -37,6 +37,7 @@ The skill generates markdown-formatted release notes following this structure: **IMPORTANT**: Always add a blank lines around the markdown code fence and output the markdown code block last **IMPORTANT**: PRs by `@gschier` should not mention the @username +**IMPORTANT**: These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process. ## After Generating Release Notes diff --git a/.codex/skills/release-generate-release-notes/SKILL.md b/.codex/skills/release-generate-release-notes/SKILL.md index cc42395c..965c4434 100644 --- a/.codex/skills/release-generate-release-notes/SKILL.md +++ b/.codex/skills/release-generate-release-notes/SKILL.md @@ -32,6 +32,7 @@ Generate formatted markdown release notes for a Yaak tag. - Keep a blank line before and after the code fence. - Output the markdown code block last. - Do not append `by @gschier` for PRs authored by `@gschier`. +- These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process. ## Release Creation Prompt From b563319bed24f296b391f4341b263b90b0c90d02 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 5 Mar 2026 16:19:05 -0800 Subject: [PATCH 09/10] Fix biome lint: update schema to 2.3.13, exclude npm dir, fix lint errors Co-Authored-By: Claude Opus 4.6 --- biome.json | 5 +++-- src-web/components/Settings/SettingsLicense.tsx | 1 - src-web/components/core/Dialog.tsx | 2 +- src-web/components/core/Editor/Editor.tsx | 2 ++ src-web/components/git/GitCommitDialog.tsx | 3 +-- src-web/components/git/GitDropdown.tsx | 2 +- src-web/lib/theme/window.ts | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/biome.json b/biome.json index 2a9cd936..2d8c601e 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json", "linter": { "enabled": true, "rules": { @@ -48,7 +48,8 @@ "!src-web/routeTree.gen.ts", "!packages/plugin-runtime-types/lib", "!**/bindings", - "!flatpak" + "!flatpak", + "!npm" ] } } diff --git a/src-web/components/Settings/SettingsLicense.tsx b/src-web/components/Settings/SettingsLicense.tsx index b398cdf3..a699b8af 100644 --- a/src-web/components/Settings/SettingsLicense.tsx +++ b/src-web/components/Settings/SettingsLicense.tsx @@ -13,7 +13,6 @@ import { Link } from '../core/Link'; import { PlainInput } from '../core/PlainInput'; import { Separator } from '../core/Separator'; import { HStack, VStack } from '../core/Stacks'; -import { LocalImage } from '../LocalImage'; export function SettingsLicense() { return ( diff --git a/src-web/components/core/Dialog.tsx b/src-web/components/core/Dialog.tsx index 5e837eb7..455e0358 100644 --- a/src-web/components/core/Dialog.tsx +++ b/src-web/components/core/Dialog.tsx @@ -5,7 +5,7 @@ import { useMemo } from 'react'; import { Overlay } from '../Overlay'; import { Heading } from './Heading'; import { IconButton } from './IconButton'; -import { DialogSize } from '@yaakapp-internal/plugins'; +import type { DialogSize } from '@yaakapp-internal/plugins'; export interface DialogProps { children: ReactNode; diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index a33cb7bd..01ff2d21 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -327,6 +327,7 @@ function EditorInner({ ); // Update the language extension when the language changes + // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally limited deps useEffect(() => { if (cm.current === null) return; const { view, languageCompartment } = cm.current; @@ -360,6 +361,7 @@ function EditorInner({ ]); // Initialize the editor when ref mounts + // biome-ignore lint/correctness/useExhaustiveDependencies: only reinitialize when necessary const initEditorRef = useCallback( function initEditorRef(container: HTMLDivElement | null) { if (container === null) { diff --git a/src-web/components/git/GitCommitDialog.tsx b/src-web/components/git/GitCommitDialog.tsx index 7de28a23..7b32730b 100644 --- a/src-web/components/git/GitCommitDialog.tsx +++ b/src-web/components/git/GitCommitDialog.tsx @@ -1,4 +1,4 @@ -import { useCachedNode } from '@dnd-kit/core/dist/hooks/utilities'; + import type { GitStatusEntry } from '@yaakapp-internal/git'; import { useGit } from '@yaakapp-internal/git'; import type { @@ -12,7 +12,6 @@ import type { import classNames from 'classnames'; import { useCallback, useMemo, useState } from 'react'; import { modelToYaml } from '../../lib/diffYaml'; -import { isSubEnvironment } from '../../lib/model_util'; import { resolvedModelName } from '../../lib/resolvedModelName'; import { showErrorToast } from '../../lib/toast'; import { Banner } from '../core/Banner'; diff --git a/src-web/components/git/GitDropdown.tsx b/src-web/components/git/GitDropdown.tsx index e80ba731..210c8452 100644 --- a/src-web/components/git/GitDropdown.tsx +++ b/src-web/components/git/GitDropdown.tsx @@ -75,7 +75,7 @@ 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 _hasRemotes = (status.data.origins ?? []).length > 0; const { ahead, behind } = status.data; const tryCheckout = (branch: string, force: boolean) => { diff --git a/src-web/lib/theme/window.ts b/src-web/lib/theme/window.ts index a5dd5939..ed54758c 100644 --- a/src-web/lib/theme/window.ts +++ b/src-web/lib/theme/window.ts @@ -138,7 +138,7 @@ function bannerColorVariables(color: YaakColor | null): Partial { }; } -function inputCSS(color: YaakColor | null): Partial { +function _inputCSS(color: YaakColor | null): Partial { if (color == null) return {}; const theme: Partial = { From 8a330ad1ec7faa7d1a48687733c46dd369126939 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Mar 2026 17:23:00 -0700 Subject: [PATCH 10/10] Auto-detect WSL and resolve Windows data directory for CLI When running the CLI in WSL, dirs::data_dir() returns the Linux path instead of the Windows host path where the Yaak desktop app stores data. This detects WSL via /proc/version, resolves %APPDATA% through cmd.exe, and converts it to a WSL mount path using wslpath. Co-Authored-By: Claude Opus 4.6 --- crates-cli/yaak-cli/src/main.rs | 48 ++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/crates-cli/yaak-cli/src/main.rs b/crates-cli/yaak-cli/src/main.rs index ba439c1f..b5a2ad30 100644 --- a/crates-cli/yaak-cli/src/main.rs +++ b/crates-cli/yaak-cli/src/main.rs @@ -10,6 +10,7 @@ mod version_check; use clap::Parser; use cli::{Cli, Commands, PluginCommands, RequestCommands}; use context::{CliContext, CliExecutionContext}; +use std::path::PathBuf; use yaak_models::queries::any_request::AnyRequest; #[tokio::main] @@ -30,9 +31,7 @@ async fn main() { let app_id = if cfg!(debug_assertions) { "app.yaak.desktop.dev" } else { "app.yaak.desktop" }; - let data_dir = data_dir.unwrap_or_else(|| { - dirs::data_dir().expect("Could not determine data directory").join(app_id) - }); + let data_dir = data_dir.unwrap_or_else(|| resolve_data_dir(app_id)); version_check::maybe_check_for_updates().await; @@ -239,3 +238,46 @@ fn resolve_cookie_jar_id( .map(|jar| jar.id); Ok(default_cookie_jar) } + +fn resolve_data_dir(app_id: &str) -> PathBuf { + if let Some(dir) = wsl_data_dir(app_id) { + return dir; + } + dirs::data_dir().expect("Could not determine data directory").join(app_id) +} + +/// Detect WSL and resolve the Windows AppData\Roaming path for the Yaak data directory. +fn wsl_data_dir(app_id: &str) -> Option { + if !cfg!(target_os = "linux") { + return None; + } + + let proc_version = std::fs::read_to_string("/proc/version").ok()?; + let is_wsl = proc_version.to_lowercase().contains("microsoft"); + if !is_wsl { + return None; + } + + // We're in WSL, so try to resolve the Yaak app's data directory in Windows + + // Get the Windows %APPDATA% path via cmd.exe + let appdata_output = + std::process::Command::new("cmd.exe").args(["/C", "echo", "%APPDATA%"]).output().ok()?; + + let win_path = String::from_utf8(appdata_output.stdout).ok()?.trim().to_string(); + if win_path.is_empty() || win_path == "%APPDATA%" { + return None; + } + + // Convert Windows path to WSL path using wslpath (handles custom mount points) + let wslpath_output = std::process::Command::new("wslpath").arg(&win_path).output().ok()?; + + let wsl_appdata = String::from_utf8(wslpath_output.stdout).ok()?.trim().to_string(); + if wsl_appdata.is_empty() { + return None; + } + + let wsl_path = PathBuf::from(wsl_appdata).join(app_id); + + if wsl_path.exists() { Some(wsl_path) } else { None } +}