From 3f3eb5938a716863be5afce440a8899192750c7c Mon Sep 17 00:00:00 2001 From: Blomios Date: Fri, 21 Aug 2026 20:45:50 +0200 Subject: [PATCH] =?UTF-8?q?fix(chat):=20capture=20du=20final=20et=20cause?= =?UTF-8?q?=20r=C3=A9elle=20des=20fins=20de=20tour=20=C2=AB=20Le=20mod?= =?UTF-8?q?=C3=A8le=20n'a=20renvoy=C3=A9=20aucune=20r=C3=A9ponse.=20=C2=BB?= =?UTF-8?q?=20=E2=80=94=20#231?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/app-tauri/src/chat.rs | 14 ++++++++ crates/app-tauri/src/commands.rs | 14 +------- crates/app-tauri/tests/chat_bridge.rs | 47 +++++++++++++-------------- crates/app-tauri/tests/dto.rs | 7 ++++ 4 files changed, 44 insertions(+), 38 deletions(-) diff --git a/crates/app-tauri/src/chat.rs b/crates/app-tauri/src/chat.rs index a0ae1675..6170e86d 100644 --- a/crates/app-tauri/src/chat.rs +++ b/crates/app-tauri/src/chat.rs @@ -35,6 +35,11 @@ use domain::ports::{ }; use crate::dto::{ReplyChunk, ReplyProgressDto, ReplyProgressKindDto, ReplyProgressStageDto}; + +/// Visible terminal message when a structured stream ends without the authoritative +/// `ReplyEvent::Final`. +pub const MODEL_RESPONSE_NOT_FINALIZED_MESSAGE: &str = + "La réponse du modèle n'a pas pu être finalisée."; use crate::stream::TauriChannelSink; /// Maximum number of recent chat chunks retained for transport reattach replay. @@ -338,3 +343,12 @@ pub fn chunk_from_event(event: ReplyEvent) -> Option { ReplyEvent::RateLimited { .. } => None, } } + +/// Terminal wire chunk for an exhausted structured stream that never produced the +/// authoritative `ReplyEvent::Final`. +#[must_use] +pub fn unfinished_turn_chunk() -> ReplyChunk { + ReplyChunk::Error { + message: MODEL_RESPONSE_NOT_FINALIZED_MESSAGE.to_owned(), + } +} diff --git a/crates/app-tauri/src/commands.rs b/crates/app-tauri/src/commands.rs index 80b258ca..73dc7d26 100644 --- a/crates/app-tauri/src/commands.rs +++ b/crates/app-tauri/src/commands.rs @@ -2512,7 +2512,6 @@ pub async fn agent_send( let meta = state.structured_sessions.meta_for_session(&sid); std::thread::spawn(move || { let mut terminal_visible = false; - let mut streamed_text = String::new(); let mut settler = crate::chat::ProgressSettler::default(); for event in stream { // Non-terminal, sans contenu chat : un signal de limite alimente le service @@ -2543,9 +2542,6 @@ pub async fn agent_send( let Some(chunk) = crate::chat::chunk_from_event(event) else { continue; }; - if let ReplyChunk::TextDelta { text } = &chunk { - streamed_text.push_str(text); - } if matches!(chunk, ReplyChunk::Final { .. } | ReplyChunk::Error { .. }) { terminal_visible = true; } @@ -2556,15 +2552,7 @@ pub async fn agent_send( let _ = bridge.send_output_settled(&sid, &mut settler, chunk); } if !terminal_visible { - let chunk = if streamed_text.trim().is_empty() { - ReplyChunk::Error { - message: "Le modèle n'a renvoyé aucune réponse.".to_owned(), - } - } else { - ReplyChunk::Final { - content: streamed_text, - } - }; + let chunk = crate::chat::unfinished_turn_chunk(); let _ = bridge.send_output_settled(&sid, &mut settler, chunk); } bridge.detach_if(&sid, gen); diff --git a/crates/app-tauri/tests/chat_bridge.rs b/crates/app-tauri/tests/chat_bridge.rs index 23b70dcb..35ee19d9 100644 --- a/crates/app-tauri/tests/chat_bridge.rs +++ b/crates/app-tauri/tests/chat_bridge.rs @@ -14,8 +14,8 @@ use std::sync::{Arc, Mutex}; use tauri::ipc::{Channel, InvokeResponseBody}; use app_tauri_lib::chat::{ - chunk_from_event, ChatBridge, ProgressSettler, MAX_CHAT_SCROLLBACK_BYTES, - MAX_CHAT_SCROLLBACK_CHUNKS, + chunk_from_event, unfinished_turn_chunk, ChatBridge, ProgressSettler, + MAX_CHAT_SCROLLBACK_BYTES, MAX_CHAT_SCROLLBACK_CHUNKS, MODEL_RESPONSE_NOT_FINALIZED_MESSAGE, }; use app_tauri_lib::dto::{ ReplyChunk, ReplyProgressDto, ReplyProgressKindDto, ReplyProgressSourceDto, @@ -233,13 +233,9 @@ fn chunk_from_event_drops_heartbeat() { /// end. This is the body of the spawned pump thread, run inline. fn pump_turn(bridge: &ChatBridge, session: &SessionId, gen: u64, events: Vec) { let mut terminal_visible = false; - let mut streamed_text = String::new(); for event in events { // Mirror the real pump: heartbeats map to no chunk and are skipped. if let Some(chunk) = chunk_from_event(event) { - if let ReplyChunk::TextDelta { text } = &chunk { - streamed_text.push_str(text); - } if matches!(chunk, ReplyChunk::Final { .. } | ReplyChunk::Error { .. }) { terminal_visible = true; } @@ -247,12 +243,7 @@ fn pump_turn(bridge: &ChatBridge, session: &SessionId, gen: u64, events: Vec