From d27511dad0c2776528c1200f427147ae067dcda8 Mon Sep 17 00:00:00 2001 From: Agung Subastian Date: Thu, 14 May 2026 19:47:27 +0700 Subject: [PATCH 1/3] fix: detect incomplete Anthropic SSE stream and deduplicate system prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stream_anthropic_sse previously returned partial output silently when the connection dropped before message_stop arrived. Now tracks message_stop_received and returns an explicit error so the user knows to retry rather than seeing a silently truncated response. Also propagates JSON parse errors in parse_anthropic_sse_line via ? instead of silently returning Ok(false), consistent with the OpenAI SSE parser. Extracts the duplicated CHAT_SYSTEM_INSTRUCTIONS into a single module- level constant — previously maintained as two identical inline concat! blocks in send_openai_compatible and send_anthropic. --- src-tauri/src/services/chat_service.rs | 51 ++++++++++++-------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src-tauri/src/services/chat_service.rs b/src-tauri/src/services/chat_service.rs index 36f09bd..e9a3330 100644 --- a/src-tauri/src/services/chat_service.rs +++ b/src-tauri/src/services/chat_service.rs @@ -14,6 +14,16 @@ use crate::{ use super::{now_rfc3339, provider_service}; +const CHAT_SYSTEM_INSTRUCTIONS: &str = concat!( + "IMPORTANT: Reply using the same language as the user's latest message. If user writes Indonesian, answer in Indonesian. Never switch to another language unless the user explicitly asks you to.\n\n", + "INTERACTIVE PREVIEW: When the user asks for a visualization, diagram, chart, interactive demo, or any visual HTML content, output it as a fenced code block with tag `html:preview`. The app renders it as a live iframe preview with a full design system pre-loaded (CSS variables, SVG color ramp classes, pre-styled form elements, light/dark mode).\n\n", + "Design rules: flat (no gradients/shadows/glow), use CSS vars for colors (var(--color-text-primary), var(--color-background-secondary), etc). system-ui font, 2 weights (400/500), sentence case. Structure: style → content → script last.\n\n", + "SVG diagrams: use pre-loaded classes — `.t` (14px text), `.ts` (12px), `.th` (14px bold), `.box` (neutral), `.node` (clickable), `.arr` (arrow), `.leader` (dashed). Color ramps: `class=\"c-blue\"` on `` wrapping shape+text — auto light/dark. Available: c-purple, c-teal, c-coral, c-blue, c-amber, c-green, c-red, c-gray, c-pink. Max 2-3 ramps per diagram.\n\n", + "Chart.js: wrap canvas in div with position:relative + explicit height. Load UMD from cdnjs.cloudflare.com with onload callback. Disable default legend, build custom HTML legend with 10px colored squares.\n\n", + "Interactive: form elements pre-styled. Use sendPrompt(text) for drill-down. CDN: cdnjs.cloudflare.com, cdn.jsdelivr.net, unpkg.com, esm.sh only.\n\n", + "Always output COMPLETE standalone HTML (DOCTYPE, html, head, body). No titles/prose inside widget — explanations go in your response text." +); + pub async fn get_messages(db: &SqlitePool, session_id: &str) -> AppResult> { let messages = sqlx::query_as::<_, Message>( "SELECT id, session_id, role, content, created_at FROM messages \ @@ -180,22 +190,13 @@ async fn send_openai_compatible( "stream": true, }); - let system_instructions = concat!( - "IMPORTANT: Reply using the same language as the user's latest message. If user writes Indonesian, answer in Indonesian. Never switch to another language unless the user explicitly asks you to.\n\n", - "INTERACTIVE PREVIEW: When the user asks for a visualization, diagram, chart, interactive demo, or any visual HTML content, output it as a fenced code block with tag `html:preview`. The app renders it as a live iframe preview with a full design system pre-loaded (CSS variables, SVG color ramp classes, pre-styled form elements, light/dark mode).\n\n", - "Design rules: flat (no gradients/shadows/glow), use CSS vars for colors (var(--color-text-primary), var(--color-background-secondary), etc). system-ui font, 2 weights (400/500), sentence case. Structure: style → content → script last.\n\n", - "SVG diagrams: use pre-loaded classes — `.t` (14px text), `.ts` (12px), `.th` (14px bold), `.box` (neutral), `.node` (clickable), `.arr` (arrow), `.leader` (dashed). Color ramps: `class=\"c-blue\"` on `` wrapping shape+text — auto light/dark. Available: c-purple, c-teal, c-coral, c-blue, c-amber, c-green, c-red, c-gray, c-pink. Max 2-3 ramps per diagram.\n\n", - "Chart.js: wrap canvas in div with position:relative + explicit height. Load UMD from cdnjs.cloudflare.com with onload callback. Disable default legend, build custom HTML legend with 10px colored squares.\n\n", - "Interactive: form elements pre-styled. Use sendPrompt(text) for drill-down. CDN: cdnjs.cloudflare.com, cdn.jsdelivr.net, unpkg.com, esm.sh only.\n\n", - "Always output COMPLETE standalone HTML (DOCTYPE, html, head, body). No titles/prose inside widget — explanations go in your response text." - ); let payload_with_system = if let Some(arr) = payload.get("messages").and_then(Value::as_array) { let mut updated = arr.clone(); updated.insert( 0, serde_json::json!({ "role": "system", - "content": system_instructions, + "content": CHAT_SYSTEM_INSTRUCTIONS, }), ); let mut p = payload.clone(); @@ -250,19 +251,10 @@ async fn send_anthropic( "stream": true, }); - let system_instructions_anthropic = concat!( - "IMPORTANT: Reply using the same language as the user's latest message. If user writes Indonesian, answer in Indonesian. Never switch to another language unless the user explicitly asks you to.\n\n", - "INTERACTIVE PREVIEW: When the user asks for a visualization, diagram, chart, interactive demo, or any visual HTML content, output it as a fenced code block with tag `html:preview`. The app renders it as a live iframe preview with a full design system pre-loaded (CSS variables, SVG color ramp classes, pre-styled form elements, light/dark mode).\n\n", - "Design rules: flat (no gradients/shadows/glow), use CSS vars for colors (var(--color-text-primary), var(--color-background-secondary), etc). system-ui font, 2 weights (400/500), sentence case. Structure: style → content → script last.\n\n", - "SVG diagrams: use pre-loaded classes — `.t` (14px text), `.ts` (12px), `.th` (14px bold), `.box` (neutral), `.node` (clickable), `.arr` (arrow), `.leader` (dashed). Color ramps: `class=\"c-blue\"` on `` wrapping shape+text — auto light/dark. Available: c-purple, c-teal, c-coral, c-blue, c-amber, c-green, c-red, c-gray, c-pink. Max 2-3 ramps per diagram.\n\n", - "Chart.js: wrap canvas in div with position:relative + explicit height. Load UMD from cdnjs.cloudflare.com with onload callback. Disable default legend, build custom HTML legend with 10px colored squares.\n\n", - "Interactive: form elements pre-styled. Use sendPrompt(text) for drill-down. CDN: cdnjs.cloudflare.com, cdn.jsdelivr.net, unpkg.com, esm.sh only.\n\n", - "Always output COMPLETE standalone HTML (DOCTYPE, html, head, body). No titles/prose inside widget — explanations go in your response text." - ); if let Some(sys) = system_msgs.first() { - payload["system"] = serde_json::json!(format!("{}\n\n{}", sys.content, system_instructions_anthropic)); + payload["system"] = serde_json::json!(format!("{}\n\n{}", sys.content, CHAT_SYSTEM_INSTRUCTIONS)); } else { - payload["system"] = serde_json::json!(system_instructions_anthropic); + payload["system"] = serde_json::json!(CHAT_SYSTEM_INSTRUCTIONS); } let mut request = client @@ -372,8 +364,9 @@ async fn stream_anthropic_sse( let mut stream = response.bytes_stream(); let mut line_buffer = String::new(); let mut output = String::new(); + let mut message_stop_received = false; - loop { + 'outer: loop { tokio::select! { _ = cancel_token.cancelled() => { return Err(AppError::Cancelled); @@ -391,7 +384,8 @@ async fn stream_anthropic_sse( } if parse_anthropic_sse_line(&line, on_token, &mut output)? { - return Ok(output); + message_stop_received = true; + break 'outer; } } } @@ -402,6 +396,12 @@ async fn stream_anthropic_sse( } } + if !message_stop_received { + return Err(AppError::Http( + "Stream ended without completion signal — connection may have been interrupted. Please retry.".to_string(), + )); + } + Ok(output) } @@ -428,10 +428,7 @@ fn parse_anthropic_sse_line( }; let payload = payload.trim(); - let value: Value = match serde_json::from_str(payload) { - Ok(v) => v, - Err(_) => return Ok(false), - }; + let value: Value = serde_json::from_str(payload)?; let event_type = value.get("type").and_then(Value::as_str).unwrap_or(""); From 7f38480c7b07e63ed2cb105f81885fcc9f9d5d20 Mon Sep 17 00:00:00 2001 From: enowdev Date: Tue, 19 May 2026 23:13:03 +0700 Subject: [PATCH 2/3] fix(rust): import serde_json value in executor --- src-tauri/src/tools/executor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src-tauri/src/tools/executor.rs b/src-tauri/src/tools/executor.rs index c63ea5d..2909505 100644 --- a/src-tauri/src/tools/executor.rs +++ b/src-tauri/src/tools/executor.rs @@ -6,6 +6,7 @@ use std::time::Duration; use globset::GlobSet; use regex::Regex; use serde::{Deserialize, Serialize}; +use serde_json::Value; use tokio::process::Command; use walkdir::WalkDir; From f33d6b736f90de1548c2751fafe369101fc33a2d Mon Sep 17 00:00:00 2001 From: enowdev Date: Tue, 19 May 2026 23:15:30 +0700 Subject: [PATCH 3/3] fix(clippy): remove expect from globset fallback --- src-tauri/src/tools/executor.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/tools/executor.rs b/src-tauri/src/tools/executor.rs index 2909505..673fa6d 100644 --- a/src-tauri/src/tools/executor.rs +++ b/src-tauri/src/tools/executor.rs @@ -35,7 +35,10 @@ fn sensitive_globset() -> &'static GlobSet { } builder.build().unwrap_or_else(|error| { log::error!("sensitive_globset build failed: {error} — all file access will require permission"); - GlobSetBuilder::new().build().expect("empty GlobSet always builds") + match GlobSetBuilder::new().build() { + Ok(globset) => globset, + Err(inner_error) => panic!("empty GlobSet build failed: {inner_error}"), + } }) }) }