use crate::client::{CommandResult, DiscoveredProjectSummary, HealthReport, ManagedSessionView};
use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup};
mod fleet;
pub use fleet::format_fleet_by_project;
#[cfg(test)]
mod tests;
const SHORT_ID_LEN: usize = 8;
pub struct TelegramFormatter;
impl TelegramFormatter {
pub fn format(result: &CommandResult) -> String {
match result {
CommandResult::Sessions(sessions) => {
if sessions.is_empty() {
return "No active sessions.".to_string();
}
let mut text = String::from("<b>trusty-mpm sessions</b>\n");
for s in sessions {
let dot = if s.status.eq_ignore_ascii_case("active") {
"🟢"
} else {
"🔴"
};
text.push_str(&format!(
"\n{dot} <code>{}</code> — {}\n 📁 <code>{}</code>\n",
short_id(&s.id),
s.status,
s.workdir,
));
}
text
}
CommandResult::SessionDetail { id, events, .. } => {
if events.is_empty() {
format!("Session {id}: no recent events")
} else {
let lines = events
.iter()
.map(|e| format!("• {e}"))
.collect::<Vec<_>>()
.join("\n");
format!("<b>Session {}</b>\n{lines}", short_id(id))
}
}
CommandResult::OverseerStatus {
enabled,
handler,
decisions,
} => format!(
"<b>Overseer Status</b>\nHandler: <code>{handler}</code>\nEnabled: {}\n\
Recent decisions: allow ({}), block ({}), flag ({})",
if *enabled { "✅" } else { "❌" },
decisions.allow,
decisions.block,
decisions.flag,
),
CommandResult::TmuxSessions(sessions) => {
if sessions.is_empty() {
return "No tmux sessions found.".to_string();
}
let lines = sessions
.iter()
.map(|s| {
let tag = if s.managed {
"🟢 managed"
} else {
"⚪ external"
};
format!("• <code>{}</code> — {tag}", s.name)
})
.collect::<Vec<_>>()
.join("\n");
format!("<b>tmux sessions</b>\n{lines}")
}
CommandResult::DiscoveredProjects(projects) => format_discovered_projects(projects),
CommandResult::Adopted { session } => {
format!("✅ Adopted tmux session <code>{session}</code> for oversight")
}
CommandResult::Discovered { count } => {
if *count == 0 {
"🔍 No new Claude Code tmux sessions found".to_string()
} else {
format!("🔍 Discovered and adopted {count} Claude Code tmux session(s)")
}
}
CommandResult::ProjectRegistered { path } => {
format!("✅ Registered project <code>{path}</code>")
}
CommandResult::ConfigAnalysis {
project,
recommendations,
} => {
if recommendations.is_empty() {
format!(
"<b>Claude config</b> for <code>{project}</code>\n\
No recommendations — config looks healthy."
)
} else {
let lines = recommendations
.iter()
.map(|r| format!("• {}", r.message))
.collect::<Vec<_>>()
.join("\n");
format!("<b>Claude config</b> for <code>{project}</code>\n{lines}")
}
}
CommandResult::Snapshot { session, output } => {
let tail: Vec<&str> = output.lines().rev().take(50).collect();
let lines = tail.into_iter().rev().collect::<Vec<_>>().join("\n");
if lines.is_empty() {
format!("Session <code>{session}</code>: empty pane")
} else {
format!(
"<b>Snapshot: {session}</b>\n<pre>{}</pre>",
html_escape(&lines)
)
}
}
CommandResult::Killed { session_id } => {
format!("🗑️ Session {} killed", short_id(session_id))
}
CommandResult::CommandSent { session, output } => {
if output.trim().is_empty() {
format!("📨 Sent to <code>{session}</code> — no output captured")
} else {
format!("<b>📨 {session}</b>\n<pre>{}</pre>", html_escape(output))
}
}
CommandResult::ChatReply { reply } => html_escape(reply),
CommandResult::Approved { session_id } => {
format!("✅ Permission approved for session {session_id}")
}
CommandResult::Denied { session_id } => {
format!("❌ Permission denied for session {session_id}")
}
CommandResult::PairCode {
code,
expires_in_seconds,
} => format!(
"<b>Pairing code:</b> <code>{code}</code>\n\
Expires in {} minutes\n\nSend to your bot: <code>/pair {code}</code>",
expires_in_seconds / 60,
),
CommandResult::PairSuccess { chat_info } => {
format!(
"✅ Successfully paired! This chat ({chat_info}) is now registered for alerts."
)
}
CommandResult::PairState { paired } => {
if *paired {
"✅ Bot is paired with this daemon. Type /help to see commands.".to_string()
} else {
"👋 Welcome to trusty-mpm bot! To pair this bot with your daemon, run \
`tm pair` on your server, then send the code with /pair <code>"
.to_string()
}
}
CommandResult::AlertSubscriptions(lines) => {
format!("<b>Alert subscription</b>\n{}", lines.join("\n"))
}
CommandResult::Doctor(report) => format_doctor_report(report),
CommandResult::SessionStarted {
session,
workdir,
deployed,
} => {
let mode = if *deployed {
"launched (framework deployed)"
} else {
"connected (no deployment)"
};
format!("✅ Session <b>{session}</b> {mode}\n<code>{workdir}</code>")
}
CommandResult::ManagedSpawned {
id,
name,
state,
runtime,
attach_cmd,
} => format!(
"✅ Spawned <b>{}</b> (<code>{}</code>) [{}] runtime={}\n\
attach: <code>{}</code>",
html_escape(name),
short_id(id),
html_escape(state),
html_escape(runtime),
html_escape(attach_cmd),
),
CommandResult::ManagedAdopted {
id,
name,
state,
cwd,
runtime,
attach_cmd,
} => format!(
"✅ Adopted <b>{}</b> (<code>{}</code>) [{}] runtime={}\n\
cwd: <code>{}</code>\n\
attach: <code>{}</code>",
html_escape(name),
short_id(id),
html_escape(state),
html_escape(runtime),
html_escape(cwd),
html_escape(attach_cmd),
),
CommandResult::ManagedSessions(sessions) => format_managed_sessions(sessions),
CommandResult::ManagedFleet(fleet) => format_fleet_by_project(fleet),
CommandResult::ManagedSession(view) => format_managed_session(view),
CommandResult::ManagedSent { id, tmux_name } => {
format!(
"📨 Sent to managed <code>{}</code> ({})",
html_escape(tmux_name),
short_id(id)
)
}
CommandResult::ManagedAnswered {
id,
answer,
tmux_name,
} => format!(
"✅ Answered decision on <code>{}</code> ({}) → <code>{}</code>",
html_escape(tmux_name),
short_id(id),
html_escape(answer),
),
CommandResult::ManagedActivity {
id,
state,
summary,
pending_decision,
..
} => {
let decision = pending_decision
.as_deref()
.map(|d| format!("\n⚠️ pending: {}", html_escape(d)))
.unwrap_or_default();
format!(
"<b>Activity {} [{}]</b>\n{}{decision}",
short_id(id),
html_escape(state),
html_escape(summary),
)
}
CommandResult::ManagedAttachCmd { id, attach_cmd } => {
format!(
"<b>Attach {}</b>\n<code>{}</code>",
short_id(id),
html_escape(attach_cmd),
)
}
CommandResult::ManagedLifecycle {
id,
name,
state,
action,
} => format!(
"✅ {} ({}) {} → {}",
html_escape(name),
short_id(id),
html_escape(action),
html_escape(state),
),
CommandResult::Help(text) => html_escape(text),
CommandResult::Health(report) => format_health(report),
CommandResult::Error(msg) => format!("❌ {}", html_escape(msg)),
}
}
pub fn keyboard_for(result: &CommandResult) -> Option<InlineKeyboardMarkup> {
match result {
CommandResult::Sessions(sessions) if !sessions.is_empty() => {
let rows: Vec<Vec<InlineKeyboardButton>> = sessions
.iter()
.map(|s| {
vec![
InlineKeyboardButton::callback("📋 Status", format!("status:{}", s.id)),
InlineKeyboardButton::callback(
"✅ Approve",
format!("approve:{}", s.id),
),
InlineKeyboardButton::callback("❌ Deny", format!("deny:{}", s.id)),
]
})
.collect();
Some(InlineKeyboardMarkup::new(rows))
}
CommandResult::DiscoveredProjects(projects) if !projects.is_empty() => {
let rows: Vec<Vec<InlineKeyboardButton>> = projects
.iter()
.filter(|p| callback_fits(&p.path))
.map(|p| {
vec![InlineKeyboardButton::callback(
format!("📁 Set Active — {}", project_basename(&p.path)),
format!("setproj:{}", p.path),
)]
})
.collect();
if rows.is_empty() {
None
} else {
Some(InlineKeyboardMarkup::new(rows))
}
}
CommandResult::TmuxSessions(sessions) => {
let rows: Vec<Vec<InlineKeyboardButton>> = sessions
.iter()
.filter(|s| !s.managed && callback_fits(&s.name))
.map(|s| {
vec![InlineKeyboardButton::callback(
format!("➕ Adopt — {}", s.name),
format!("adopt:{}", s.name),
)]
})
.collect();
if rows.is_empty() {
None
} else {
Some(InlineKeyboardMarkup::new(rows))
}
}
CommandResult::ManagedFleet(fleet) => fleet::focus_keyboard(fleet),
CommandResult::ManagedSession(view) => {
fleet::session_focus_keyboard(&view.id, &view.name)
}
_ => None,
}
}
}
pub fn format_discovered_projects(projects: &[DiscoveredProjectSummary]) -> String {
if projects.is_empty() {
return "No projects discovered in Claude Code config.".to_string();
}
let mut text = String::from("<b>Discovered projects</b>\n");
for p in projects {
let last = p
.last_session
.as_deref()
.map(|s| s.split('T').next().unwrap_or(s).to_string())
.unwrap_or_else(|| "never".to_string());
text.push_str(&format!(
"\n📁 <code>{}</code>\n {} session(s) · last used {last}\n",
p.path, p.session_count,
));
}
text
}
pub fn format_managed_sessions(sessions: &[ManagedSessionView]) -> String {
if sessions.is_empty() {
return "No managed sessions.".to_string();
}
let mut text = String::from("<b>managed sessions</b>\n");
for s in sessions {
let flag = if s.pending_decision.is_some() {
" ⚠️"
} else {
""
};
text.push_str(&format!(
"\n• <code>{}</code> {} [{}]{flag}",
short_id(&s.id),
html_escape(&s.name),
html_escape(&s.state),
));
}
text
}
pub fn format_managed_session(view: &ManagedSessionView) -> String {
let mut text = format!(
"<b>{}</b> (<code>{}</code>) [{}]",
html_escape(&view.name),
short_id(&view.id),
html_escape(&view.state),
);
if let Some(ws) = &view.workspace_path {
text.push_str(&format!("\n📁 <code>{}</code>", html_escape(ws)));
}
if let Some(repo) = &view.repo_url {
let branch = view.branch.as_deref().unwrap_or("");
text.push_str(&format!(
"\n🔗 {} {}",
html_escape(repo),
html_escape(branch)
));
}
if let Some(d) = &view.pending_decision {
let def = view.proposed_default.as_deref().unwrap_or("");
text.push_str(&format!(
"\n⚠️ pending: {} (default: <code>{}</code>)",
html_escape(d),
html_escape(def),
));
}
text
}
fn format_doctor_report(report: &crate::client::DoctorReport) -> String {
let mut text = String::from("<b>trusty-mpm doctor</b>\n");
for check in &report.checks {
text.push_str(&format!(
"\n{} <b>{}</b> — {}",
status_icon(check.status),
html_escape(&check.name),
html_escape(&check.message),
));
}
text.push_str(&format!(
"\n\n{} <b>overall: {}</b>",
status_icon(report.overall),
status_word(report.overall),
));
text
}
fn format_health(report: &HealthReport) -> String {
if !report.reachable {
return format!(
"❌ <b>daemon unreachable</b>\n<code>{}</code>",
html_escape(&report.url),
);
}
let catalog = if report.catalog_unknown {
"catalog: never synced"
} else if report.catalog_stale {
"catalog: ⚠️ updates available"
} else {
"catalog: up to date"
};
format!(
"✅ <b>daemon {}</b>\n<code>{}</code>\n{}\nfleet: {} session(s), {} awaiting a decision",
html_escape(&report.status),
html_escape(&report.url),
catalog,
report.managed_total,
report.managed_pending_decisions,
)
}
fn status_icon(status: crate::core::doctor::CheckStatus) -> &'static str {
use crate::core::doctor::CheckStatus;
match status {
CheckStatus::Ok => "✅",
CheckStatus::Warn => "⚠️",
CheckStatus::Fail => "❌",
}
}
fn status_word(status: crate::core::doctor::CheckStatus) -> &'static str {
use crate::core::doctor::CheckStatus;
match status {
CheckStatus::Ok => "healthy",
CheckStatus::Warn => "warnings",
CheckStatus::Fail => "failed",
}
}
fn callback_fits(arg: &str) -> bool {
arg.len() <= 55
}
fn project_basename(path: &str) -> &str {
path.rsplit('/')
.next()
.filter(|s| !s.is_empty())
.unwrap_or(path)
}
pub(crate) fn short_id(id: &str) -> String {
if id.len() > SHORT_ID_LEN {
format!("{}…", &id[..SHORT_ID_LEN])
} else {
id.to_string()
}
}
pub fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}