use std::path::PathBuf;
pub const SPINNER_FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
pub type Provider = (
&'static str,
&'static str,
&'static [(&'static str, &'static str)],
);
pub const PROVIDERS: &[Provider] = &[
(
"Anthropic (Claude)",
"ANTHROPIC_KEY",
&[
(
"anthropic/claude-sonnet-4-5",
"Claude Sonnet 4.5 (recommended)",
),
("anthropic/claude-opus-4", "Claude Opus 4"),
("anthropic/claude-haiku-3-5", "Claude Haiku 3.5"),
],
),
(
"OpenAI (GPT)",
"OPENAI_KEY",
&[
("openai/gpt-4o", "GPT-4o (recommended)"),
("openai/gpt-4o-mini", "GPT-4o Mini"),
],
),
(
"Google (Gemini)",
"GOOGLE_KEY",
&[
("google/gemini-2.0-flash", "Gemini 2.0 Flash (recommended)"),
("google/gemini-2.5-pro", "Gemini 2.5 Pro"),
],
),
];
pub const CORE_COMPONENTS: &[&str] = &[
"asterbot:agent",
"asterbot:core",
"asterbot:toolkit",
"asterai:llm",
];
pub const DEFAULT_TOOLS: &[&str] = &["asterbot:soul", "asterbot:memory", "asterbot:skills"];
pub const SLASH_COMMANDS: &[SlashCommand] = &[
SlashCommand {
name: "help",
description: "Show available commands",
},
SlashCommand {
name: "tools",
description: "List, add, or remove tools",
},
SlashCommand {
name: "clear",
description: "Clear conversation history",
},
SlashCommand {
name: "model",
description: "View or switch LLM model",
},
SlashCommand {
name: "name",
description: "View or change agent name",
},
SlashCommand {
name: "dir",
description: "Manage directory access",
},
SlashCommand {
name: "status",
description: "Show agent status",
},
SlashCommand {
name: "config",
description: "Manage env variables",
},
SlashCommand {
name: "push",
description: "Push agent to cloud",
},
SlashCommand {
name: "pull",
description: "Pull agent from cloud",
},
];
pub struct SlashCommand {
pub name: &'static str,
pub description: &'static str,
}
pub enum Screen {
Auth(AuthState),
Picker(PickerState),
Setup(SetupState),
Chat(ChatState),
}
pub enum AuthState {
Checking,
NeedLogin {
input: String,
error: Option<String>,
},
LoggingIn,
}
pub enum SetupStep {
Name,
Provider,
ApiKey,
Model,
Directories,
Provisioning {
current: usize,
total: usize,
message: String,
},
WarmUp,
PushPrompt,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum MessageRole {
User,
Assistant,
System,
}
#[derive(Debug, Clone)]
pub struct AgentConfig {
pub env_name: String,
pub bot_name: String,
pub model: Option<String>,
pub provider: String,
pub tools: Vec<String>,
pub allowed_dirs: Vec<String>,
}
#[derive(Clone)]
pub struct AgentEntry {
pub name: String,
pub namespace: String,
pub component_count: usize,
pub bot_name: String,
pub model: Option<String>,
}
pub struct App {
pub screen: Screen,
pub should_quit: bool,
pub agent: Option<AgentConfig>,
pub pending_response: Option<tokio::sync::oneshot::Receiver<eyre::Result<Option<String>>>>,
}
impl Default for App {
fn default() -> Self {
Self {
screen: Screen::Auth(AuthState::Checking),
should_quit: false,
agent: None,
pending_response: None,
}
}
}
impl App {
pub fn push_message(&mut self, role: MessageRole, content: String) {
if let Screen::Chat(state) = &mut self.screen {
state.messages.push(ChatMessage { role, content });
}
}
}
pub struct PickerState {
pub agents: Vec<AgentEntry>,
pub selected: usize,
pub loading: bool,
}
pub struct SetupState {
pub step: SetupStep,
pub bot_name: String,
pub env_name: String,
pub provider_idx: usize,
pub api_key: String,
pub model: String,
pub model_idx: usize,
pub allowed_dirs: Vec<String>,
pub input: String,
pub error: Option<String>,
}
impl Default for SetupState {
fn default() -> Self {
Self {
step: SetupStep::Name,
bot_name: String::new(),
env_name: String::new(),
provider_idx: 0,
api_key: String::new(),
model: String::new(),
model_idx: 0,
allowed_dirs: Vec::new(),
input: String::new(),
error: None,
}
}
}
pub struct ChatMessage {
pub role: MessageRole,
pub content: String,
}
#[derive(Default)]
pub struct ChatState {
pub messages: Vec<ChatMessage>,
pub input: String,
pub input_history: Vec<String>,
pub history_idx: Option<usize>,
pub waiting: bool,
pub spinner_tick: usize,
pub slash_matches: Vec<usize>,
pub slash_selected: usize,
pub scroll_offset: u16,
}
pub fn resolve_state_dir(env_name: &str) -> PathBuf {
dirs::home_dir()
.expect("could not determine home directory")
.join(".asterai-agents")
.join(env_name)
}
pub fn sanitize_bot_name(display_name: &str) -> String {
let sanitized: String = display_name
.to_lowercase()
.chars()
.map(|c| match c.is_ascii_alphanumeric() || c == '-' {
true => c,
false => '-',
})
.collect();
let trimmed = sanitized.trim_matches('-').replace("--", "-");
let end = trimmed.len().min(40);
let result = &trimmed[..end];
match result.is_empty() {
true => "asterbot".to_string(),
false => result.to_string(),
}
}