use super::*;
pub(super) async fn refresh_pending_messages(state: &Arc<TuiState>, lane: &Arc<dyn AgentLane>) {
if let Ok(queued) = lane.queued_messages().await {
state.set_pending_queue(queued);
}
}
pub(super) fn transcript_has_live_panel(
bash_components_present: bool,
tool_components: &HashMap<String, Arc<ToolExecutionComponent>>,
) -> bool {
bash_components_present
|| tool_components
.values()
.any(|component| component.status() == ToolStatus::Running)
}
pub(super) fn merge_queued_into_draft(queued_text: &str, current: &str) -> String {
if current.trim().is_empty() {
queued_text.to_string()
} else {
format!("{queued_text}\n\n{current}")
}
}
pub(super) fn set_editor_text_caret_at_end(editor: &Arc<Editor>, text: &str) {
editor.set_text(text);
let last_row = text.split('\n').count().saturating_sub(1);
let last_len = text.split('\n').next_back().map(str::len).unwrap_or(0);
editor.set_cursor(last_row, last_len);
}
pub(super) async fn restore_queued_messages_to_editor(
lane: Arc<dyn AgentLane>,
editor: Arc<Editor>,
state: Arc<TuiState>,
tui: Arc<TuiAltScreen>,
) -> usize {
match lane.clear_queue().await {
Ok(queued) => {
state.restore_pending_images(queued.images.clone());
let all: Vec<String> = queued
.steering
.iter()
.chain(queued.follow_up.iter())
.cloned()
.collect();
if !all.is_empty() {
let queued_text = all.join("\n\n");
let current = editor.get_expanded_text();
let combined = merge_queued_into_draft(&queued_text, ¤t);
set_editor_text_caret_at_end(&editor, &combined);
}
refresh_pending_messages(&state, &lane).await;
tui.request_render(false);
all.len()
}
Err(error) => {
add_error_message(
&state.chat_container,
&format!("Could not restore queued messages: {error}"),
);
tui.request_render(false);
0
}
}
}
pub(super) async fn handle_dequeue(
lane: Arc<dyn AgentLane>,
editor: Arc<Editor>,
state: Arc<TuiState>,
tui: Arc<TuiAltScreen>,
) {
let restored =
restore_queued_messages_to_editor(lane, editor, state.clone(), tui.clone()).await;
if restored == 0 {
add_note_message(&state.chat_container, "No queued messages to restore");
} else {
let plural = if restored > 1 { "s" } else { "" };
add_note_message(
&state.chat_container,
&format!("Restored {restored} queued message{plural} to editor"),
);
}
tui.request_render(false);
}
pub(super) fn abort_run_restoring_queue(
lane: Arc<dyn AgentLane>,
editor: Arc<Editor>,
state: Arc<TuiState>,
tui: Arc<TuiAltScreen>,
) {
tokio::spawn(async move {
let _ = restore_queued_messages_to_editor(lane.clone(), editor, state, tui.clone()).await;
let _ = lane.abort().await;
tui.request_render(false);
});
}
pub(super) async fn drain_agent_events(
mut rx: broadcast::Receiver<AgentEvent>,
tui: Arc<TuiAltScreen>,
state: Arc<TuiState>,
chat: Arc<Container>,
lane: Arc<dyn AgentLane>,
) {
loop {
match rx.recv().await {
Ok(event) => handle_agent_event(event, &tui, &state, &chat, &lane).await,
Err(broadcast::error::RecvError::Lagged(_)) => {
continue;
}
Err(broadcast::error::RecvError::Closed) => break,
}
}
}
pub(super) async fn handle_agent_event(
event: AgentEvent,
tui: &Arc<TuiAltScreen>,
state: &Arc<TuiState>,
chat: &Arc<Container>,
lane: &Arc<dyn AgentLane>,
) {
match event {
AgentEvent::AgentStart => {
state.set_status(RunStatus::Working);
tui.request_render(false);
}
AgentEvent::AgentEnd { .. } => {
state.transcript_view().finalize_assistant();
state.set_status(RunStatus::Idle);
let pending: Vec<AgentMessage> =
std::mem::take(&mut *state.pending_bash_messages.lock().unwrap());
for message in pending {
if let Err(error) = lane.append_message(message).await {
add_error_message(chat, &format!("Could not record bash result: {error}"));
}
}
refresh_pending_messages(state, lane).await;
tui.request_render(false);
}
AgentEvent::RetryScheduled {
attempt,
max_retries,
delay_ms,
..
} => {
state.show_retry(attempt, max_retries, delay_ms);
tui.request_render(false);
}
AgentEvent::TurnStart => {
state.transcript_view().finalize_assistant();
}
AgentEvent::TurnEnd {
message,
tool_results,
} => {
let view = state.transcript_view();
if let AgentMessage::Assistant(a) = &message {
view.finalize_assistant_blocks(&assistant_blocks(a));
} else {
view.finalize_assistant();
}
let _ = tool_results;
tui.request_render(false);
}
AgentEvent::MessageStart { message } => match message {
AgentMessage::Assistant(a) => {
state.transcript_view().apply(
&UiEvent::AssistantStart {
blocks: assistant_blocks(&a),
},
tui.width(),
);
tui.request_render(false);
}
AgentMessage::Custom(custom) => {
let payload = serde_json::json!({
"customType": custom.role,
"content": custom.content,
"details": custom.data,
"expanded": false,
"outputPad": 1,
});
if let Some(component) = extension_message_component(
&state.extension_session,
&custom.role,
&payload,
state.markdown_transformer(),
) {
chat.add_child(component);
chat.add_child(Arc::new(Spacer::new(1)));
tui.request_render(false);
} else {
add_note_message(chat, &custom_message_fallback(&custom));
tui.request_render(false);
}
}
AgentMessage::User(user) => {
state.transcript_view().add_user(&user_message_text(&user));
add_user_images(&state.chat_container, &user, state.images_visible());
refresh_pending_messages(state, lane).await;
tui.request_render(false);
}
_ => {}
},
AgentEvent::MessageUpdate {
message,
assistant_message_event,
} => {
let _ = assistant_message_event; let a: &rpi_ai::types::AssistantMessage = &message;
let text = assistant_text(a);
state.transcript_view().apply(
&UiEvent::AssistantUpdate {
blocks: assistant_blocks(a),
tool_calls: assistant_tool_calls(a),
},
tui.width(),
);
state.sync_working_loader_with_bash();
*state.last_assistant_text.lock().unwrap() = text;
tui.request_render(false);
}
AgentEvent::MessageEnd { message } => {
if let AgentMessage::Assistant(a) = &message {
let text = assistant_text(a);
state
.transcript_view()
.finalize_assistant_blocks(&assistant_blocks(a));
if !text.is_empty() {
*state.last_assistant_text.lock().unwrap() = text;
}
let usage = &a.usage;
state.footer.add_usage(
usage.input,
usage.output,
usage.cache_read,
usage.cache_write,
usage.cost.total,
);
let denom = usage.input + usage.cache_read + usage.cache_write;
if denom > 0 && (usage.cache_read + usage.cache_write) > 0 {
state
.footer
.set_cache_hit_rate(Some(usage.cache_read as f64 / denom as f64 * 100.0));
}
record_context_usage(&state.footer, a);
let miss = state
.cache_tracker
.lock()
.unwrap()
.observe(a, &rpi_harness::cache_stats::NoPrices);
let cache_miss_notices = *state.cache_miss_notices.lock().unwrap();
if let Some(miss) = miss.filter(|_| cache_miss_notices) {
let cost = if miss.missed_cost > 0.0 {
format!(" (~${:.4})", miss.missed_cost)
} else {
String::new()
};
add_note_message(
&state.chat_container,
&format!(
"Cache miss: {} tokens re-billed{}",
format_tokens(miss.missed_tokens),
cost
),
);
}
if let Some(text) = extension_usage_text(Some(&state.extension_session), usage) {
add_note_message(chat, &text);
}
if let Some(error) = assistant_error_text(a) {
add_error_message(chat, &error);
}
}
tui.request_render(false);
}
AgentEvent::ToolExecutionStart {
tool_call_id,
tool_name,
args,
} => {
state.transcript_view().apply(
&UiEvent::ToolStart {
id: tool_call_id,
name: tool_name,
args,
},
tui.width(),
);
state.sync_working_loader_with_bash();
tui.request_render(false);
}
AgentEvent::ToolExecutionUpdate {
tool_call_id,
tool_name,
args,
partial_result,
} => {
state.transcript_view().apply(
&UiEvent::ToolUpdate {
id: tool_call_id,
name: tool_name,
args,
result: crate::remote::protocol::tool_result_json(&partial_result),
},
tui.width(),
);
state.sync_working_loader_with_bash();
tui.request_render(false);
}
AgentEvent::ToolExecutionEnd {
tool_call_id,
tool_name,
result,
is_error,
} => {
state.transcript_view().apply(
&UiEvent::ToolEnd {
id: tool_call_id,
name: tool_name,
result: crate::remote::protocol::tool_result_json(&result),
is_error,
},
tui.width(),
);
add_tool_images(
&state.chat_container,
&result.content,
state.images_visible(),
);
state.sync_working_loader_with_bash();
tui.render_now(false);
}
}
}
pub(super) fn record_context_usage(
footer: &rpi_tui::FooterComponent,
message: &rpi_ai::AssistantMessage,
) {
if message.stop_reason == rpi_ai::StopReason::Aborted
|| message.stop_reason == rpi_ai::StopReason::Error
{
return;
}
let tokens = message.usage.context_tokens();
if tokens > 0 {
footer.set_context_tokens(Some(tokens));
}
}
pub(super) fn assistant_error_text(message: &rpi_ai::AssistantMessage) -> Option<String> {
let fallback = match message.stop_reason {
rpi_ai::StopReason::Error => "Provider request failed.",
rpi_ai::StopReason::Aborted => "Request aborted.",
_ => return None,
};
Some(
message
.error_message
.as_deref()
.filter(|text| !text.trim().is_empty())
.unwrap_or(fallback)
.to_string(),
)
}
pub(super) fn tool_result_requests_markdown(details: Option<&serde_json::Value>) -> bool {
details
.and_then(|details| details.get("markdown"))
.and_then(|value| value.as_bool())
.unwrap_or(false)
}