Kill TextDelta, Info — UiMessage is dead. RAII ActivityGuards replace all status feedback

Streaming text now goes directly to agent entries via append_streaming().
sync_from_agent diffs the growing entry each tick. The streaming entry
is popped when the response completes; build_response_message pushes
the final version.

All status feedback uses RAII ActivityGuards:
- push_activity() for long-running work (thinking, streaming, scoring)
- notify() for instant feedback (compacted, DMN state changes, commands)
- Guards auto-remove on Drop, appending "(complete)" and lingering 5s
- expire_activities() cleans up timed-out notifications on render tick

UiMessage enum reduced to a single Info variant with zero sends.
The channel infrastructure remains for now (Mind/Agent still take
UiSender in signatures) — mechanical cleanup for a follow-up.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-05 22:18:07 -04:00
parent e7914e3d58
commit cfddb55ed9
9 changed files with 201 additions and 186 deletions

View file

@ -20,7 +20,7 @@ use tokio::sync::mpsc;
use crate::agent::tools::{self as agent_tools, summarize_args, ActiveToolCall};
pub use types::ToolCall;
use crate::user::ui_channel::{UiMessage, UiSender, StreamTarget};
use crate::user::ui_channel::UiSender;
/// A JoinHandle that aborts its task when dropped.
pub struct AbortOnDrop(tokio::task::JoinHandle<()>);
@ -130,7 +130,7 @@ impl ApiClient {
&reasoning_effort, sampling, priority,
).await;
if let Err(e) = result {
let _ = tx.send(StreamEvent::Error(e.to_string();
let _ = tx.send(StreamEvent::Error(e.to_string()));
}
});
@ -207,7 +207,6 @@ pub(crate) async fn send_and_check(
body: &impl serde::Serialize,
auth_header: (&str, &str),
extra_headers: &[(&str, &str)],
ui_tx: &UiSender,
debug_label: &str,
request_json: Option<&str>,
) -> Result<reqwest::Response> {
@ -619,8 +618,6 @@ pub struct StreamResult {
/// - UI forwarding (text deltas, reasoning, tool call notifications)
pub async fn collect_stream(
rx: &mut mpsc::UnboundedReceiver<StreamEvent>,
ui_tx: &UiSender,
target: StreamTarget,
agent: &std::sync::Arc<tokio::sync::Mutex<super::Agent>>,
active_tools: &crate::user::ui_channel::SharedActiveTools,
) -> StreamResult {
@ -633,12 +630,13 @@ pub async fn collect_stream(
let mut error = None;
let mut first_content = true;
let mut display_buf = String::new();
let mut _streaming_guard: Option<super::ActivityGuard> = None;
while let Some(event) = rx.recv().await {
match event {
StreamEvent::Content(text) => {
if first_content {
if let Ok(mut ag) = agent.try_lock() { ag.activity = "streaming...".into(); }
_streaming_guard = Some(super::start_activity(agent, "streaming...").await);
first_content = false;
}
content.push_str(&text);
@ -683,7 +681,7 @@ pub async fn collect_stream(
if let Some(pos) = display_buf.find("<tool_call>") {
let before = &display_buf[..pos];
if !before.is_empty() {
let _ = ui_tx.send(UiMessage::TextDelta(before.to_string(), target));
if let Ok(mut ag) = agent.try_lock() { ag.append_streaming(before); }
}
display_buf.clear();
in_tool_call = true;
@ -693,7 +691,7 @@ pub async fn collect_stream(
if safe > 0 {
let flush = display_buf[..safe].to_string();
display_buf = display_buf[safe..].to_string();
let _ = ui_tx.send(UiMessage::TextDelta(flush, target));
if let Ok(mut ag) = agent.try_lock() { ag.append_streaming(&flush); }
}
}
}