From 20afdfa1378992d96e925eab664b5d8946fe759c Mon Sep 17 00:00:00 2001 From: Anoop Narang Date: Mon, 4 May 2026 15:23:40 +0530 Subject: [PATCH 1/3] feat(wizard): render schema description, examples, and defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schema-driven prompts in `connections new` previously ignored every JSON Schema annotation except `default` (and only on plain string fields). Even when the API attached descriptive metadata to a field, the wizard still showed bare `field_name:` with the only hint being "optional — press Enter to skip" when the field was non-required. Surface that metadata at the prompt: - `description` is rendered as inquire help text on every supported field type (string, password, integer, boolean, array). When the field is also optional, the description and the existing "optional — press Enter to skip" hint are concatenated rather than one displacing the other. - `examples[0]` is rendered as the inquire placeholder for plain strings, integers, and arrays when no explicit `default` is set. Password fields skip the placeholder (placeholders alongside hidden input read awkwardly in terminals); their description still carries any example shape. This is a generic improvement keyed on schema content, not on connector type — every connector that adds description / examples to its connection-types schema picks it up automatically. --- src/connections_new.rs | 60 +++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/src/connections_new.rs b/src/connections_new.rs index da8a5ef..51e4885 100644 --- a/src/connections_new.rs +++ b/src/connections_new.rs @@ -113,14 +113,25 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option { let field_type = field["type"].as_str().unwrap_or("string"); let format = field["format"].as_str().unwrap_or(""); + let description = field["description"].as_str(); + let example = field["examples"] + .as_array() + .and_then(|a| a.first()) + .and_then(|v| v.as_str()); let opt_hint = "optional — press Enter to skip"; + let help_message: Option = match (description, is_required) { + (Some(d), true) => Some(d.to_string()), + (Some(d), false) => Some(format!("{d} ({opt_hint})")), + (None, true) => None, + (None, false) => Some(opt_hint.to_string()), + }; match (field_type, format) { ("string", "password") => { let label = format!("{key}:"); let mut p = Password::new(&label).without_confirmation(); - if !is_required { - p = p.with_help_message(opt_hint); + if let Some(h) = &help_message { + p = p.with_help_message(h); } let val = p.prompt().unwrap_or_else(|_| std::process::exit(0)); if val.is_empty() && !is_required { @@ -133,11 +144,14 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option { ("string", _) => { let label = format!("{key}:"); let mut t = Text::new(&label); - if let Some(default) = field["default"].as_str() { - t = t.with_default(default); + let default = field["default"].as_str(); + if let Some(d) = default { + t = t.with_default(d); + } else if let Some(e) = example { + t = t.with_placeholder(e); } - if !is_required { - t = t.with_help_message(opt_hint); + if let Some(h) = &help_message { + t = t.with_help_message(h); } let val = t.prompt().unwrap_or_else(|_| std::process::exit(0)); if val.is_empty() && !is_required { @@ -149,7 +163,7 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option { ("integer", _) => { let label = format!("{key}:"); - let t = Text::new(&label).with_validator(move |input: &str| { + let mut t = Text::new(&label).with_validator(move |input: &str| { if input.is_empty() { if is_required { return Ok(Validation::Invalid("This field is required".into())); @@ -162,13 +176,12 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option { Ok(Validation::Invalid("Must be a whole number".into())) } }); - let help_t; - let t = if !is_required { - help_t = t.with_help_message(opt_hint); - help_t - } else { - t - }; + if let Some(e) = example { + t = t.with_placeholder(e); + } + if let Some(h) = &help_message { + t = t.with_help_message(h); + } let val = t.prompt().unwrap_or_else(|_| std::process::exit(0)); if val.is_empty() && !is_required { None @@ -182,23 +195,28 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option { ("boolean", _) => { let label = format!("{key}:"); let default = field["default"].as_bool().unwrap_or(false); - let val = Confirm::new(&label) - .with_default(default) - .prompt() - .unwrap_or_else(|_| std::process::exit(0)); + let mut c = Confirm::new(&label).with_default(default); + if let Some(h) = &help_message { + c = c.with_help_message(h); + } + let val = c.prompt().unwrap_or_else(|_| std::process::exit(0)); Some(Value::Bool(val)) } ("array", _) => { let label = format!("{key}:"); - let help = if is_required { + let array_hint = if is_required { "Enter values separated by commas" } else { "Enter values separated by commas — optional, press Enter to skip" }; + let help = match description { + Some(d) => format!("{d} — {array_hint}"), + None => array_hint.to_string(), + }; let val = Text::new(&label) - .with_placeholder("value1, value2, ...") - .with_help_message(help) + .with_placeholder(example.unwrap_or("value1, value2, ...")) + .with_help_message(&help) .prompt() .unwrap_or_else(|_| std::process::exit(0)); if val.is_empty() && !is_required { From 2e5944de0b2bb53dd7985ad161e775258f7d27f7 Mon Sep 17 00:00:00 2001 From: Anoop Narang Date: Mon, 4 May 2026 15:23:51 +0530 Subject: [PATCH 2/3] fix(api): add request timeout and tcp keepalive The blocking HTTP client was built via reqwest::blocking::Client::new() with no explicit configuration, which on macOS surfaces as "error sending request" when a request is in flight long enough for the OS to drop the quiet TCP connection (e.g. while the server is doing slow synchronous work like ducklake schema discovery against a remote catalog). Add an explicit overall request timeout (5 min) to bound the worst case if the server genuinely hangs, and a 30s TCP keepalive so the socket stays warm across long synchronous server work. Both values live as constants near the helper for clarity. --- src/api.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/api.rs b/src/api.rs index 4824af9..50271c0 100644 --- a/src/api.rs +++ b/src/api.rs @@ -3,6 +3,27 @@ use crate::config; use crate::util; use crossterm::style::Stylize; use serde::de::DeserializeOwned; +use std::time::Duration; + +/// Cap on any single HTTP request. Connection create + synchronous schema +/// discovery against a slow remote catalog can take well over a minute, so +/// this needs to be generous; 5 minutes leaves headroom while still bounding +/// the worst case if the server genuinely hangs. +const HTTP_REQUEST_TIMEOUT: Duration = Duration::from_secs(300); + +/// TCP keepalive cadence. Without this, macOS will drop a TCP connection +/// that has been quiet (e.g. while the server is doing slow synchronous +/// work) and reqwest surfaces it as "error sending request" even though the +/// request itself completed server-side. +const TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(30); + +fn build_http_client() -> reqwest::blocking::Client { + reqwest::blocking::Client::builder() + .timeout(HTTP_REQUEST_TIMEOUT) + .tcp_keepalive(TCP_KEEPALIVE_INTERVAL) + .build() + .expect("reqwest blocking client should always build with these defaults") +} #[derive(Clone)] pub struct ApiClient { @@ -48,7 +69,7 @@ impl ApiClient { }; Self { - client: reqwest::blocking::Client::new(), + client: build_http_client(), api_key: access_token, api_url: profile_config.api_url.to_string(), workspace_id: workspace_id.map(String::from), @@ -66,7 +87,7 @@ impl ApiClient { #[cfg(test)] pub(crate) fn test_new(api_url: &str, api_key: &str, workspace_id: Option<&str>) -> Self { Self { - client: reqwest::blocking::Client::new(), + client: build_http_client(), api_key: api_key.to_string(), api_url: api_url.to_string(), workspace_id: workspace_id.map(String::from), From ddfb5de5cc48564687c31d3141798286856b6d6d Mon Sep 17 00:00:00 2001 From: Anoop Narang Date: Mon, 4 May 2026 18:04:06 +0530 Subject: [PATCH 3/3] fix(wizard): accept integer examples in prompt placeholders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous extraction used `as_str()` only, which silently returned None for numeric JSON examples. So a schema like `"examples": [8080]` on an integer field — exactly the kind of example that would benefit from a placeholder — produced no placeholder despite the integer branch wiring one up. Widen the extraction to also accept i64 and coerce to String, then pass through call sites via `as_deref()`. No schema in runtimedb today carries numeric examples, so this is a pure consistency fix — but it aligns the code with what the original commit's description claimed to support. --- src/connections_new.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/connections_new.rs b/src/connections_new.rs index 51e4885..4a231df 100644 --- a/src/connections_new.rs +++ b/src/connections_new.rs @@ -114,10 +114,16 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option { let field_type = field["type"].as_str().unwrap_or("string"); let format = field["format"].as_str().unwrap_or(""); let description = field["description"].as_str(); - let example = field["examples"] + // Accept both string and integer examples — `as_str` alone would silently + // miss schemas like `"examples": [8080]` on integer fields. + let example: Option = field["examples"] .as_array() .and_then(|a| a.first()) - .and_then(|v| v.as_str()); + .and_then(|v| { + v.as_str() + .map(str::to_owned) + .or_else(|| v.as_i64().map(|n| n.to_string())) + }); let opt_hint = "optional — press Enter to skip"; let help_message: Option = match (description, is_required) { (Some(d), true) => Some(d.to_string()), @@ -147,7 +153,7 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option { let default = field["default"].as_str(); if let Some(d) = default { t = t.with_default(d); - } else if let Some(e) = example { + } else if let Some(e) = example.as_deref() { t = t.with_placeholder(e); } if let Some(h) = &help_message { @@ -176,7 +182,7 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option { Ok(Validation::Invalid("Must be a whole number".into())) } }); - if let Some(e) = example { + if let Some(e) = example.as_deref() { t = t.with_placeholder(e); } if let Some(h) = &help_message { @@ -215,7 +221,7 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option { None => array_hint.to_string(), }; let val = Text::new(&label) - .with_placeholder(example.unwrap_or("value1, value2, ...")) + .with_placeholder(example.as_deref().unwrap_or("value1, value2, ...")) .with_help_message(&help) .prompt() .unwrap_or_else(|_| std::process::exit(0));