diff --git a/src-tauri/src/llm.rs b/src-tauri/src/llm.rs index e05af71..a1b8f20 100644 --- a/src-tauri/src/llm.rs +++ b/src-tauri/src/llm.rs @@ -57,27 +57,33 @@ pub async fn chat_stream( anyhow::bail!("Ollama returned {status}: {text}"); } - let mut buffer = String::new(); + let mut buffer: Vec = Vec::new(); loop { match response.chunk().await { Ok(Some(bytes)) => { - buffer.push_str(&String::from_utf8_lossy(&bytes)); - while let Some(nl) = buffer.find('\n') { - let line = buffer[..nl].trim().to_string(); + buffer.extend_from_slice(&bytes); + while let Some(nl) = buffer.iter().position(|&b| b == b'\n') { + let line_bytes = buffer[..nl].to_vec(); buffer.drain(..=nl); + let line = String::from_utf8_lossy(&line_bytes); + let line = line.trim(); if line.is_empty() { continue; } - match serde_json::from_str::(&line) { + match serde_json::from_str::(line) { Ok(chunk) if chunk.done => { - let _ = app.emit("robin:chat-done", ()); + if let Err(e) = app.emit("robin:chat-done", ()) { + log::warn!("llm: failed to emit chat-done: {e}"); + } return Ok(()); } Ok(chunk) => { if let Some(msg) = chunk.message { if !msg.content.is_empty() { - let _ = app.emit("robin:chat-token", msg.content); + if let Err(e) = app.emit("robin:chat-token", msg.content) { + log::warn!("llm: failed to emit chat-token: {e}"); + } } } } @@ -89,13 +95,15 @@ pub async fn chat_stream( } Ok(None) => break, Err(e) => { - return Err(anyhow::anyhow!("stream read error: {e}")); + return Err(e).context("stream read error"); } } } // Stream ended without a done:true line - let _ = app.emit("robin:chat-done", ()); + if let Err(e) = app.emit("robin:chat-done", ()) { + log::warn!("llm: failed to emit chat-done: {e}"); + } Ok(()) } @@ -133,10 +141,16 @@ mod tests { } #[test] - fn parse_empty_token_is_handled() { + fn parse_empty_content_chunk_deserializes() { let json = r#"{"model":"llama3.2","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","content":""},"done":false}"#; let chunk: OllamaChunk = serde_json::from_str(json).unwrap(); assert!(!chunk.done); assert_eq!(chunk.message.unwrap().content, ""); } + + #[test] + fn malformed_json_fails_to_parse() { + let result = serde_json::from_str::("not valid json"); + assert!(result.is_err(), "malformed JSON must fail to parse"); + } }