use slack_morphism::prelude::*;
#[derive(Debug, Clone)]
pub(crate) enum GroupEntry {
Tool {
name: String,
context: String,
status: Option<bool>,
},
Note(String),
}
impl GroupEntry {
fn is_running(&self) -> bool {
matches!(self, Self::Tool { status: None, .. })
}
fn is_failed(&self) -> bool {
matches!(
self,
Self::Tool {
status: Some(false),
..
}
)
}
fn is_tool(&self) -> bool {
matches!(self, Self::Tool { .. })
}
}
#[derive(Debug, Clone)]
pub(crate) struct GroupState {
pub channel: SlackChannelId,
pub entries: Vec<GroupEntry>,
pub expanded: bool,
}
fn entry_icon(status: Option<bool>) -> &'static str {
match status {
None => "⚙️",
Some(true) => "✅",
Some(false) => "❌",
}
}
pub(crate) fn notes_text(entries: &[GroupEntry]) -> Option<String> {
let joined = entries
.iter()
.filter_map(|e| match e {
GroupEntry::Note(text) => Some(text.trim()),
GroupEntry::Tool { .. } => None,
})
.filter(|t| !t.is_empty())
.collect::<Vec<_>>()
.join("\n\n");
(!joined.is_empty()).then_some(joined)
}
fn entry_line(entry: &GroupEntry) -> String {
match entry {
GroupEntry::Tool {
name,
context,
status,
} => format!("{} *{}*{}", entry_icon(*status), name, context),
GroupEntry::Note(text) => format!("💭 _{}_", text.trim()),
}
}
fn summary_line(entries: &[GroupEntry]) -> String {
let steps = entries.len();
let tools = entries.iter().filter(|e| e.is_tool()).count();
let running = entries.iter().filter(|e| e.is_running()).count();
let failed = entries.iter().filter(|e| e.is_failed()).count();
let (icon, tail) = if running > 0 {
("⚙️", format!(" · {running} running"))
} else if failed > 0 {
("❌", format!(" · {failed} failed"))
} else {
("✅", String::new())
};
let counts = if steps == tools {
format!("{tools} tool call{}", if tools == 1 { "" } else { "s" })
} else {
format!(
"{steps} step{} · {tools} tool call{}",
if steps == 1 { "" } else { "s" },
if tools == 1 { "" } else { "s" }
)
};
format!("{icon} *{counts}*{tail}")
}
pub(crate) fn render(group: &GroupState, ts: &SlackTs) -> SlackMessageContent {
let text = if group.entries.len() == 1 && group.expanded {
entry_line(&group.entries[0])
} else if group.expanded {
let lines: Vec<String> = group.entries.iter().map(entry_line).collect();
format!("{}\n{}", summary_line(&group.entries), lines.join("\n"))
} else if group.entries.len() == 1 {
entry_line(&group.entries[0])
} else {
summary_line(&group.entries)
};
let mut blocks = vec![SlackBlock::Section(SlackSectionBlock::new().with_text(
SlackBlockText::MarkDown(SlackBlockMarkDownText::new(text.clone())),
))];
if group.entries.len() > 1 {
let label = if group.expanded {
"Collapse ▲"
} else {
"Expand ▼"
};
blocks.push(SlackBlock::Actions(SlackActionsBlock::new(vec![
SlackActionBlockElement::Button(SlackBlockButtonElement::new(
SlackActionId::new(format!("toolgroup:{}", ts)),
SlackBlockPlainTextOnly::from(SlackBlockPlainText::new(label.to_string())),
)),
])));
}
SlackMessageContent::new()
.with_text(text)
.with_blocks(blocks)
}