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:
2026-08-11 18:49:12 +02:00
parent bc9a034249
commit ee0ffd4e2d
16 changed files with 654 additions and 61 deletions

View File

@ -688,7 +688,7 @@ impl ReplyProgress {
/// Ajoute un extrait textuel.
#[must_use]
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self.text = bounded_reply_progress_preview(text.into());
self
}
@ -712,6 +712,57 @@ impl ReplyProgress {
}
}
fn bounded_reply_progress_preview(text: String) -> Option<String> {
let mut lines = Vec::new();
let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
for line in normalized.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
lines.push(trimmed.to_owned());
if lines.len() == 4 {
break;
}
}
if lines.is_empty() {
None
} else {
Some(lines.join("\n"))
}
}
#[cfg(test)]
mod reply_progress_tests {
use super::{ReplyProgress, ReplyProgressKind, ReplyProgressSource, ReplyProgressStage};
#[test]
fn with_text_keeps_first_four_non_blank_lines() {
let progress = ReplyProgress::new(
ReplyProgressSource::ProviderNative,
ReplyProgressKind::Tool,
ReplyProgressStage::Started,
"tool",
)
.with_text(" one \n\n two\r\n three\r four\n five ");
assert_eq!(progress.text.as_deref(), Some("one\ntwo\nthree\nfour"));
}
#[test]
fn with_text_drops_blank_preview() {
let progress = ReplyProgress::new(
ReplyProgressSource::ProviderNative,
ReplyProgressKind::Tool,
ReplyProgressStage::Started,
"tool",
)
.with_text(" \n\t\r\n ");
assert_eq!(progress.text, None);
}
}
/// Un événement incrémental d'un tour de réponse d'un agent IA (ARCHITECTURE §17.1).
///
/// Universel : l'adapter (Claude/Codex) traduit SON format structuré documenté