fix(chat): réattache persistante CLI custom et rendu tools/MCP/progress enrichi (#188)
Corrige la perte de session à la navigation: le démontage du CustomAgentChatView appelait close_agent_session (destructeur) au lieu de simplement détacher le transport. Désormais: - nouvelle commande Tauri detach_agent_chat (+ ChatBridge::detach) qui libère le canal sans tuer la session ni le scrollback retenu; distinct de close_agent_session qui reste le close destructeur explicite; - le frontend appelle detachAgentChat à l'unmount puis restore sa vue depuis un cache de session (turns + chunkCount) à la réattache; - message « Reprise effectuée: N événements rattrapés… » quand la réattache rejoue plus de chunks que le cache local n'en avait conservé. Enrichit le rendu tools/MCP/progress: - ProgressSettler (chat bridge) émet un progress completed synthétique quand un spinner reste ouvert (nouvelle entité progress, chunk text/final/error): fini les spinners fantômes; - les adapters claude/codex/opencode/openai_compat extraient un aperçu du payload (arguments/input/command/message/params) attaché au progress; - bounded_reply_progress_preview (domain) garde les 4 premières lignes non vides, normalisation CR/LF; - previewText (frontend) multiligne whitespace-pre-wrap, source/nativeEvent affichés sous le titre; suppression du flag settled et du Spinner inline devenus redondants (l'état est porté par le stage + le settler). Tests: detach_agent_chat gateway (Tauri + mock), ProgressSettler, bornage reply_progress, item_preview/tool_preview par adapter, réattache avec cache et notice de rattrapage.
This commit is contained in:
@ -2379,6 +2379,7 @@ pub async fn agent_send(
|
||||
let tap_bridge = std::sync::Arc::clone(&state.chat_bridge);
|
||||
let tap_sid = sid;
|
||||
let tap_pump = std::thread::spawn(move || {
|
||||
let mut settler = crate::chat::ProgressSettler::default();
|
||||
for event in tap_rx {
|
||||
let Some(chunk) = crate::chat::chunk_from_event(event) else {
|
||||
continue;
|
||||
@ -2386,7 +2387,10 @@ pub async fn agent_send(
|
||||
if matches!(chunk, ReplyChunk::Final { .. } | ReplyChunk::Error { .. }) {
|
||||
continue;
|
||||
}
|
||||
let _ = tap_bridge.send_output(&tap_sid, chunk);
|
||||
let _ = tap_bridge.send_output_settled(&tap_sid, &mut settler, chunk);
|
||||
}
|
||||
if let Some(completed) = settler.finish() {
|
||||
let _ = tap_bridge.send_output(&tap_sid, completed);
|
||||
}
|
||||
});
|
||||
|
||||
@ -2411,6 +2415,7 @@ 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 settler = crate::chat::ProgressSettler::default();
|
||||
for event in stream {
|
||||
// Non-terminal, sans contenu chat : un signal de limite alimente le service
|
||||
// (le badge UI vient du bus `AgentRateLimited`, pas du flux chat) puis on
|
||||
@ -2447,11 +2452,12 @@ pub async fn agent_send(
|
||||
// boolean only reflects live delivery. If the view navigated away
|
||||
// (no channel at this generation) we keep draining so the turn still
|
||||
// completes and the scrollback stays whole for the next re-attach.
|
||||
let _ = bridge.send_output(&sid, chunk);
|
||||
let _ = bridge.send_output_settled(&sid, &mut settler, chunk);
|
||||
}
|
||||
if !terminal_visible {
|
||||
let _ = bridge.send_output(
|
||||
let _ = bridge.send_output_settled(
|
||||
&sid,
|
||||
&mut settler,
|
||||
ReplyChunk::Error {
|
||||
message: "Le modèle n'a renvoyé aucune réponse.".to_owned(),
|
||||
},
|
||||
@ -2967,6 +2973,28 @@ pub fn reattach_agent_chat(
|
||||
})
|
||||
}
|
||||
|
||||
/// `detach_agent_chat` — detach only the custom chat view transport from a live
|
||||
/// structured session, preserving the agent process and retained scrollback.
|
||||
///
|
||||
/// This is the backend distinction between a disappearing view
|
||||
/// (layout/tab/project switch, cell unmount) and [`close_agent_session`], which is
|
||||
/// an explicit destructive close.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if no live
|
||||
/// structured session owns the id).
|
||||
#[tauri::command]
|
||||
pub fn detach_agent_chat(session_id: String, state: State<'_, AppState>) -> Result<(), ErrorDto> {
|
||||
let sid = parse_session_id(&session_id)?;
|
||||
if state.structured_sessions.session(&sid).is_none() {
|
||||
return Err(ErrorDto::from(AppError::NotFound(format!(
|
||||
"structured session {sid}"
|
||||
))));
|
||||
}
|
||||
state.chat_bridge.detach(&sid);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `close_agent_session` — shut a live structured session down and tear its
|
||||
/// transport (channel + conversation scrollback) down (ARCHITECTURE §17.7).
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user