From 39b5746019ff26d804f6f5d3a69c8b8fcc15aa03 Mon Sep 17 00:00:00 2001 From: Ajain Date: Mon, 16 Mar 2026 22:07:53 +1100 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20client=20improvements=20=E2=80=94?= =?UTF-8?q?=20plugin-store,=20native=20SSE=20streaming,=20UI=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Storage - Replace OS keychain (keyring crate) with tauri-plugin-store for API key persistence — eliminates macOS keychain authorization prompts entirely - Keys stored in app data directory (~/Library/Application Support/com.reasondb.desktop/api-keys.json) ## Streaming - Route REASON query SSE through a native Rust reqwest command (5-min timeout) to bypass WKWebView's internal resource timeout that cuts long-running REASON queries before the LLM completes - Keep fetch-based fallback for non-Tauri (browser dev) environments ## UI - StatusBar now fetches live table count, document count, and server version instead of showing hardcoded "3 tables · 150 documents · v0.1.0" - Welcome screen: replace generic Database icon with ReasonDB logo SVG - Welcome screen: update subtitle and feature descriptions to match reasondb.ai - Fix documentation URL → https://reasondb.ai/ - Fix GitHub URL → https://github.com/brainfish-ai/ReasonDB --- Cargo.lock | 114 +++++---------- apps/reasondb-client/package.json | 1 + apps/reasondb-client/src-tauri/Cargo.toml | 4 +- .../src-tauri/capabilities/default.json | 5 + apps/reasondb-client/src-tauri/src/lib.rs | 133 ++++++++++++++---- .../src/components/common/WelcomeScreen.tsx | 22 +-- .../src/components/layout/Sidebar.tsx | 2 +- .../src/components/layout/StatusBar.tsx | 60 +++++++- apps/reasondb-client/src/lib/api.ts | 48 +++++-- apps/reasondb-client/src/lib/keychain.ts | 41 ++++-- yarn.lock | 7 + 11 files changed, 289 insertions(+), 148 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddcebbf..3210596 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1503,27 +1503,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dbus" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "dbus-secret-service" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" -dependencies = [ - "dbus", - "zeroize", -] - [[package]] name = "deranged" version = "0.5.8" @@ -3178,21 +3157,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "keyring" -version = "3.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" -dependencies = [ - "byteorder", - "dbus-secret-service", - "log", - "security-framework 2.11.1", - "security-framework 3.7.0", - "windows-sys 0.60.2", - "zeroize", -] - [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -3253,15 +3217,6 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" -[[package]] -name = "libdbus-sys" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" -dependencies = [ - "pkg-config", -] - [[package]] name = "libloading" version = "0.7.4" @@ -3598,7 +3553,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework 3.7.0", + "security-framework", "security-framework-sys", "tempfile", ] @@ -4845,14 +4800,16 @@ dependencies = [ name = "reasondb" version = "0.2.1" dependencies = [ - "keyring", + "futures-util", "log", + "reqwest 0.12.28", "serde", "serde_json", "tauri", "tauri-build", "tauri-plugin-log", "tauri-plugin-shell", + "tauri-plugin-store", ] [[package]] @@ -5149,6 +5106,7 @@ dependencies = [ "base64 0.22.1", "bytes", "futures-core", + "futures-util", "http 1.4.0", "http-body 1.0.1", "http-body-util", @@ -5168,12 +5126,14 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-rustls 0.26.4", + "tokio-util", "tower 0.5.3", "tower-http 0.6.8", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams 0.4.2", "web-sys", "webpki-roots 1.0.6", ] @@ -5208,7 +5168,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", ] @@ -5460,7 +5420,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.7.0", + "security-framework", ] [[package]] @@ -5629,19 +5589,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework" version = "3.7.0" @@ -6690,6 +6637,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-store" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca1a8ff83c269b115e98726ffc13f9e548a10161544a92ad121d6d0a96e16ea" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "tauri-runtime" version = "2.10.1" @@ -7756,6 +7719,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -8695,20 +8671,6 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] [[package]] name = "zerotrie" diff --git a/apps/reasondb-client/package.json b/apps/reasondb-client/package.json index 3e9e1dd..f425ce4 100644 --- a/apps/reasondb-client/package.json +++ b/apps/reasondb-client/package.json @@ -43,6 +43,7 @@ "@tauri-apps/plugin-dialog": "^2.6.0", "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-shell": "^2.3.5", + "@tauri-apps/plugin-store": "^2.4.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "d3": "^7.9.0", diff --git a/apps/reasondb-client/src-tauri/Cargo.toml b/apps/reasondb-client/src-tauri/Cargo.toml index e9f88af..0b75d01 100644 --- a/apps/reasondb-client/src-tauri/Cargo.toml +++ b/apps/reasondb-client/src-tauri/Cargo.toml @@ -22,4 +22,6 @@ log = "0.4" tauri = { version = "2.9.5", features = [] } tauri-plugin-log = "2" tauri-plugin-shell = "2" -keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] } +tauri-plugin-store = "2" +reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] } +futures-util = { version = "0.3", default-features = false, features = ["std"] } diff --git a/apps/reasondb-client/src-tauri/capabilities/default.json b/apps/reasondb-client/src-tauri/capabilities/default.json index 885c615..967b159 100644 --- a/apps/reasondb-client/src-tauri/capabilities/default.json +++ b/apps/reasondb-client/src-tauri/capabilities/default.json @@ -16,6 +16,11 @@ "core:window:allow-toggle-maximize", "core:window:allow-is-maximized", "core:window:allow-set-focus", + "store:allow-get", + "store:allow-set", + "store:allow-delete", + "store:allow-save", + "store:allow-load", "shell:allow-open", { "identifier": "shell:allow-execute", diff --git a/apps/reasondb-client/src-tauri/src/lib.rs b/apps/reasondb-client/src-tauri/src/lib.rs index eca267e..ea26ba8 100644 --- a/apps/reasondb-client/src-tauri/src/lib.rs +++ b/apps/reasondb-client/src-tauri/src/lib.rs @@ -1,34 +1,121 @@ -const KEYCHAIN_SERVICE: &str = "com.reasondb.desktop"; +use futures_util::StreamExt; +use serde_json::Value; +use tauri::ipc::Channel; +/// Execute a streaming RQL query via reqwest, bypassing the WKWebView HTTP stack. +/// +/// WKWebView (used by Tauri on macOS) has an internal resource timeout that +/// fires on long-running requests regardless of SSE keep-alive heartbeats. +/// REASON queries can take 30–120 s, so they are routed through this native +/// Rust command which uses a dedicated reqwest client with a 5-minute timeout. +/// +/// Progress events are forwarded to the frontend via the `on_progress` +/// Channel; the final QueryServerResponse is returned as the Ok value. #[tauri::command] -fn store_api_key(connection_id: String, api_key: String) -> Result<(), String> { - let entry = keyring::Entry::new(KEYCHAIN_SERVICE, &connection_id).map_err(|e| e.to_string())?; - entry.set_password(&api_key).map_err(|e| e.to_string()) -} +async fn execute_reason_stream( + base_url: String, + query: String, + api_key: Option, + on_progress: Channel, +) -> Result { + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(300)) + .build() + .map_err(|e| e.to_string())?; -#[tauri::command] -fn get_api_key(connection_id: String) -> Result, String> { - let entry = keyring::Entry::new(KEYCHAIN_SERVICE, &connection_id).map_err(|e| e.to_string())?; - match entry.get_password() { - Ok(key) => Ok(Some(key)), - Err(keyring::Error::NoEntry) => Ok(None), - Err(e) => Err(e.to_string()), + let url = format!("{}/v1/query/stream", base_url); + + let mut req = client + .post(&url) + .header("Content-Type", "application/json") + .json(&serde_json::json!({ "query": query })); + + if let Some(key) = api_key { + req = req.header("X-API-Key", key); } -} -#[tauri::command] -fn delete_api_key(connection_id: String) -> Result<(), String> { - let entry = keyring::Entry::new(KEYCHAIN_SERVICE, &connection_id).map_err(|e| e.to_string())?; - match entry.delete_credential() { - Ok(()) => Ok(()), - Err(keyring::Error::NoEntry) => Ok(()), // already gone — not an error - Err(e) => Err(e.to_string()), + let response = req.send().await.map_err(|e| e.to_string())?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + // Try to pull out a JSON "message" field; fall back to raw body. + let message = serde_json::from_str::(&body) + .ok() + .and_then(|v| v.get("message").and_then(|m| m.as_str()).map(String::from)) + .unwrap_or(body); + return Err(format!("HTTP {}: {}", status, message)); } + + let mut byte_stream = response.bytes_stream(); + let mut buffer = String::new(); + let mut event_type = String::new(); + let mut event_data = String::new(); + + while let Some(chunk) = byte_stream.next().await { + let bytes = chunk.map_err(|e| e.to_string())?; + buffer.push_str(&String::from_utf8_lossy(&bytes)); + + // Consume all complete lines from the buffer. + while let Some(pos) = buffer.find('\n') { + let line = buffer[..pos].trim_end_matches('\r').to_string(); + buffer = buffer[pos + 1..].to_string(); + + if line.starts_with(':') { + // SSE comment (": heartbeat") — keep-alive, ignore. + continue; + } else if let Some(rest) = line.strip_prefix("event:") { + event_type = rest.trim().to_string(); + } else if let Some(rest) = line.strip_prefix("data:") { + event_data = rest.trim().to_string(); + } else if line.is_empty() { + if event_type.is_empty() || event_data.is_empty() { + // Blank separator without a complete event — skip. + event_type.clear(); + event_data.clear(); + continue; + } + match event_type.as_str() { + "progress" => { + if let Ok(data) = serde_json::from_str::(&event_data) { + // Best-effort send — ignore if the frontend closed the channel. + let _ = on_progress.send(data); + } + } + "complete" => { + return serde_json::from_str::(&event_data) + .map_err(|e| e.to_string()); + } + "error" => { + return Err(event_data); + } + _ => {} + } + event_type.clear(); + event_data.clear(); + } + } + } + + // The byte stream ended — check if a final event is still in the buffer + // (server omitted the trailing blank line). + if !event_type.is_empty() && !event_data.is_empty() { + match event_type.as_str() { + "complete" => { + return serde_json::from_str::(&event_data).map_err(|e| e.to_string()); + } + "error" => return Err(event_data), + _ => {} + } + } + + Err("Stream ended without results".to_string()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_store::Builder::default().build()) .plugin(tauri_plugin_shell::init()) .setup(|app| { if cfg!(debug_assertions) { @@ -40,11 +127,7 @@ pub fn run() { } Ok(()) }) - .invoke_handler(tauri::generate_handler![ - store_api_key, - get_api_key, - delete_api_key, - ]) + .invoke_handler(tauri::generate_handler![execute_reason_stream]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/apps/reasondb-client/src/components/common/WelcomeScreen.tsx b/apps/reasondb-client/src/components/common/WelcomeScreen.tsx index 97776ef..c952a52 100644 --- a/apps/reasondb-client/src/components/common/WelcomeScreen.tsx +++ b/apps/reasondb-client/src/components/common/WelcomeScreen.tsx @@ -1,5 +1,4 @@ import { - Database, PlugsConnected, BookOpen, Lightning, @@ -9,6 +8,7 @@ import { import { open } from '@tauri-apps/plugin-shell' import { cn } from '@/lib/utils' import { useConnectionStore } from '@/stores/connectionStore' +import logoUrl from '@/assets/logo.svg' interface WelcomeScreenProps { onNewConnection: () => void @@ -20,20 +20,20 @@ export function WelcomeScreen({ onNewConnection }: WelcomeScreenProps) { const features = [ { icon: Brain, - title: 'REASON Queries', - description: 'Ask questions in natural language and get intelligent answers', + title: 'Hierarchical Reasoning', + description: 'LLM-guided tree traversal over your documents. Not chunks, not embeddings.', color: 'text-mauve', }, { icon: MagnifyingGlass, - title: 'Semantic Search', - description: 'Find documents by meaning, not just keywords', + title: 'Full Audit Trail', + description: '4-phase trace per query. Logged, replayable, and auditable.', color: 'text-blue', }, { icon: Lightning, - title: 'Fast & Efficient', - description: 'Built with Rust for blazing fast performance', + title: 'Built in Rust', + description: 'Single binary, deploy anywhere, bring your own LLM', color: 'text-yellow', }, ] @@ -44,8 +44,8 @@ export function WelcomeScreen({ onNewConnection }: WelcomeScreenProps) { {/* Header */}
-
-
@@ -53,7 +53,7 @@ export function WelcomeScreen({ onNewConnection }: WelcomeScreenProps) { Welcome to ReasonDB

- The AI-native database for intelligent document management + The AI database that reasons beyond RAG

@@ -83,7 +83,7 @@ export function WelcomeScreen({ onNewConnection }: WelcomeScreenProps) {