use crate::client::{FocusOutcome, InjectOutcome, SessionProxy, SummarizeOutcome};
use super::formatter::{html_escape, short_id};
const NO_FOCUS_HINT: &str =
"No session is focused — use <code>/focus <session></code> first.";
const MAX_SUMMARY_CHARS: usize = 700;
fn truncate_summary(s: &str) -> std::borrow::Cow<'_, str> {
if s.chars().count() <= MAX_SUMMARY_CHARS {
return std::borrow::Cow::Borrowed(s);
}
let truncated: String = s.chars().take(MAX_SUMMARY_CHARS).collect();
std::borrow::Cow::Owned(format!("{truncated} […truncated]"))
}
pub fn conv(chat_id: i64) -> String {
chat_id.to_string()
}
pub async fn handle_focus(proxy: &SessionProxy, chat_id: i64, arg: &str) -> String {
render_focus(&proxy.focus(&conv(chat_id), arg).await)
}
pub fn handle_unfocus(proxy: &SessionProxy, chat_id: i64) -> String {
match proxy.unfocus(&conv(chat_id)) {
Some(f) => format!(
"✅ Unfocused <b>{}</b>. Plain messages now go to the coordinator.",
html_escape(&f.name),
),
None => "No session was focused.".to_string(),
}
}
pub async fn handle_summary(proxy: &SessionProxy, chat_id: i64) -> String {
render_summary(&proxy.summarize(&conv(chat_id)).await)
}
pub async fn inject_reply(proxy: &SessionProxy, chat_id: i64, text: &str) -> String {
render_inject(&proxy.inject(&conv(chat_id), text).await)
}
fn render_focus(outcome: &FocusOutcome) -> String {
match outcome {
FocusOutcome::Focused(f) => format!(
"🎯 Focused on <b>{}</b> (<code>{}</code>).\n\
Plain messages now route to this session; /unfocus to stop.",
html_escape(&f.name),
short_id(&f.id),
),
FocusOutcome::Current(Some(f)) => format!(
"🎯 Focused on <b>{}</b> (<code>{}</code>).\n\
Plain messages route here; /unfocus to stop.",
html_escape(&f.name),
short_id(&f.id),
),
FocusOutcome::Current(None) => "Usage: <code>/focus <session></code> — focus a \
managed session so plain messages route straight to it."
.to_string(),
FocusOutcome::NotFound { target, error } => format!(
"❌ Cannot focus <code>{}</code>: {}",
html_escape(target),
html_escape(error),
),
}
}
fn render_inject(outcome: &InjectOutcome) -> String {
match outcome {
InjectOutcome::Sent { target, text } => format!(
"📨 → <b>{}</b> (<code>{}</code>)\n<i>{}</i>",
html_escape(&target.name),
short_id(&target.id),
html_escape(text),
),
InjectOutcome::AutoUnfocused { target, error } => format!(
"⚠️ Focused session <b>{}</b> is gone — focus cleared, back to the \
coordinator. ({})",
html_escape(&target.name),
html_escape(error),
),
InjectOutcome::Failed { target, error } => format!(
"❌ Send to <b>{}</b> failed: {}\nStill focused; /unfocus to stop.",
html_escape(&target.name),
html_escape(error),
),
InjectOutcome::NoFocus => NO_FOCUS_HINT.to_string(),
}
}
fn render_summary(outcome: &SummarizeOutcome) -> String {
match outcome {
SummarizeOutcome::Summary {
target,
state,
summary,
pending_decision,
} => {
let decision = pending_decision
.as_deref()
.map(|d| format!("\n⚠️ pending: {}", html_escape(d)))
.unwrap_or_default();
format!(
"<b>📋 {}</b> (<code>{}</code>) [{}]\n{}{decision}",
html_escape(&target.name),
short_id(&target.id),
html_escape(state),
html_escape(&truncate_summary(summary)),
)
}
SummarizeOutcome::AutoUnfocused { target, error } => format!(
"⚠️ Focused session <b>{}</b> is gone — focus cleared, back to the \
coordinator. ({})",
html_escape(&target.name),
html_escape(error),
),
SummarizeOutcome::Failed { target, error } => format!(
"❌ Summary of <b>{}</b> failed: {}",
html_escape(&target.name),
html_escape(error),
),
SummarizeOutcome::NoFocus => NO_FOCUS_HINT.to_string(),
}
}
#[cfg(test)]
mod tests;