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

@ -153,15 +153,17 @@ fn assistant_events(value: &Value) -> Vec<ReplyEvent> {
.and_then(Value::as_str)
.unwrap_or("outil")
.to_owned();
events.push(ReplyEvent::Progress {
progress: provider_progress(
ReplyProgressKind::Tool,
ReplyProgressStage::Started,
label.clone(),
"tool_use",
)
.with_tool_name(label.clone()),
});
let mut progress = provider_progress(
ReplyProgressKind::Tool,
ReplyProgressStage::Started,
label.clone(),
"tool_use",
)
.with_tool_name(label.clone());
if let Some(preview) = tool_use_preview(block) {
progress = progress.with_text(preview);
}
events.push(ReplyEvent::Progress { progress });
events.push(ReplyEvent::ToolActivity { label });
}
_ => {}
@ -170,6 +172,22 @@ fn assistant_events(value: &Value) -> Vec<ReplyEvent> {
events
}
fn tool_use_preview(block: &Value) -> Option<String> {
block
.get("input")
.or_else(|| block.get("arguments"))
.or_else(|| block.get("parameters"))
.map(preview_value)
.filter(|text| !text.trim().is_empty())
}
fn preview_value(value: &Value) -> String {
value
.as_str()
.map(str::to_owned)
.unwrap_or_else(|| value.to_string())
}
fn provider_progress(
kind: ReplyProgressKind,
stage: ReplyProgressStage,