use crate::client::{CommandResult, DiscoveredProjectSummary};
use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup};
#[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::Help(text) => text.clone(),
CommandResult::Error(msg) => format!("โ {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))
}
}
_ => 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
}
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 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)
}
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('>', ">")
}