use anyhow::Result;
use async_trait::async_trait;
use tokio::sync::mpsc;
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct ChannelId {
pub platform: String,
pub channel: String,
pub thread: Option<String>,
}
impl ChannelId {
pub fn new(platform: impl Into<String>, channel: impl Into<String>) -> Self {
Self {
platform: platform.into(),
channel: channel.into(),
thread: None,
}
}
pub fn with_thread(mut self, thread: impl Into<String>) -> Self {
self.thread = Some(thread.into());
self
}
pub fn display_key(&self) -> String {
match &self.thread {
Some(t) => format!("{}:{}:{}", self.platform, self.channel, t),
None => format!("{}:{}", self.platform, self.channel),
}
}
}
impl std::fmt::Display for ChannelId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.display_key())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StreamingLevel {
#[default]
Compact,
Full,
}
impl StreamingLevel {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"compact" | "c" => Some(Self::Compact),
"full" | "f" => Some(Self::Full),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WorkspaceScope {
#[default]
Project,
Workspace,
Full,
}
impl WorkspaceScope {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"project" | "p" => Some(Self::Project),
"workspace" | "w" => Some(Self::Workspace),
"full" | "f" => Some(Self::Full),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub enum RemoteEvent {
Text(String),
Response(String),
ToolCall { name: String, summary: String },
ToolResult {
name: String,
preview: String,
success: bool,
},
PlanReady { plan: String },
Done { iterations: u32, elapsed_secs: u64 },
Status(String),
}
pub struct IncomingCommand {
pub channel: ChannelId,
pub user_id: String,
pub command: super::commands::RemoteCommand,
}
#[async_trait]
pub trait PlatformAdapter: Send + Sync + 'static {
fn platform_name(&self) -> &str;
fn max_message_length(&self) -> usize;
async fn send_typing(&self, _channel: &ChannelId) -> Result<()> {
Ok(())
}
async fn send_message(&self, channel: &ChannelId, text: &str) -> Result<()>;
async fn send_long_message(
&self,
channel: &ChannelId,
text: &str,
filename: Option<&str>,
) -> Result<()>;
async fn send_buttons(
&self,
channel: &ChannelId,
text: &str,
buttons: &[(String, String)],
) -> Result<()>;
async fn edit_message(
&self,
channel: &ChannelId,
message_id: &str,
new_text: &str,
) -> Result<bool>;
async fn register_commands(&self, _commands: &[(&str, &str)]) -> Result<()> {
Ok(())
}
async fn run(&self, command_tx: mpsc::UnboundedSender<IncomingCommand>) -> Result<()>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn channel_id_display() {
let ch = ChannelId::new("telegram", "12345");
assert_eq!(ch.display_key(), "telegram:12345");
let ch = ChannelId::new("slack", "C01").with_thread("ts123");
assert_eq!(ch.display_key(), "slack:C01:ts123");
}
#[test]
fn streaming_level_parse() {
assert_eq!(
StreamingLevel::parse("compact"),
Some(StreamingLevel::Compact)
);
assert_eq!(StreamingLevel::parse("Full"), Some(StreamingLevel::Full));
assert_eq!(StreamingLevel::parse("unknown"), None);
}
#[test]
fn workspace_scope_parse() {
assert_eq!(
WorkspaceScope::parse("project"),
Some(WorkspaceScope::Project)
);
assert_eq!(WorkspaceScope::parse("W"), Some(WorkspaceScope::Workspace));
assert_eq!(WorkspaceScope::parse("full"), Some(WorkspaceScope::Full));
}
}