use std::collections::HashMap;
use std::path::PathBuf;
use crate::app::McpServerConfig;
use crate::models::ChatMessage;
use crate::models::ReasoningLevel;
use crate::models::tool_call::ToolCall as ModelToolCall;
use crate::session::ConversationHistory;
use super::compaction::{CompactionArchive, CompactionRequest};
use super::ids::{ToolCallId, TurnId};
#[derive(Debug, Clone)]
pub enum Cmd {
CallModel { turn: TurnId, request: ChatRequest },
CompactConversation {
turn: TurnId,
request: CompactionRequest,
},
ExecuteTool {
turn: TurnId,
call_id: ToolCallId,
source: ModelToolCall,
model_id: String,
},
CancelScope(TurnId),
SaveConversation(ConversationHistory),
SaveCompactionArchive(CompactionArchive),
PersistLastModel(String),
PersistReasoningFor {
model_id: String,
level: ReasoningLevel,
},
RefreshInstructions,
LoadConversation(String),
ListConversations,
InitMcpServers(HashMap<String, McpServerConfig>),
StopMcpServer { name: String },
PullOllamaModel { model: String },
OpenInSystem(PathBuf),
DismissStatusAfter { ms: u64 },
WriteImageToTemp {
path: PathBuf,
bytes: Vec<u8>,
format: String,
},
ReadClipboard,
Exit,
SetTerminalTitle(String),
}
#[derive(Debug, Clone)]
pub struct ChatRequest {
pub model_id: String,
pub messages: Vec<ChatMessage>,
pub system_prompt: String,
pub instructions: Option<String>,
pub reasoning: ReasoningLevel,
pub temperature: f32,
pub max_tokens: usize,
pub tools: Vec<ToolDefinition>,
}
#[derive(Debug, Clone)]
pub struct ToolDefinition {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
impl ToolDefinition {
pub fn to_openai_json(&self) -> serde_json::Value {
serde_json::json!({
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": self.input_schema,
}
})
}
}
impl Cmd {
pub fn tag(&self) -> &'static str {
match self {
Cmd::CallModel { .. } => "call_model",
Cmd::CompactConversation { .. } => "compact_conversation",
Cmd::ExecuteTool { .. } => "execute_tool",
Cmd::CancelScope(_) => "cancel_scope",
Cmd::SaveConversation(_) => "save_conversation",
Cmd::SaveCompactionArchive(_) => "save_compaction_archive",
Cmd::PersistLastModel(_) => "persist_last_model",
Cmd::PersistReasoningFor { .. } => "persist_reasoning_for",
Cmd::RefreshInstructions => "refresh_instructions",
Cmd::LoadConversation(_) => "load_conversation",
Cmd::ListConversations => "list_conversations",
Cmd::InitMcpServers(_) => "init_mcp_servers",
Cmd::StopMcpServer { .. } => "stop_mcp_server",
Cmd::PullOllamaModel { .. } => "pull_ollama_model",
Cmd::OpenInSystem(_) => "open_in_system",
Cmd::DismissStatusAfter { .. } => "dismiss_status_after",
Cmd::WriteImageToTemp { .. } => "write_image_to_temp",
Cmd::ReadClipboard => "read_clipboard",
Cmd::Exit => "exit",
Cmd::SetTerminalTitle(_) => "set_terminal_title",
}
}
pub fn is_turn_scoped(&self) -> bool {
matches!(
self,
Cmd::CallModel { .. } | Cmd::CompactConversation { .. } | Cmd::ExecuteTool { .. }
)
}
pub fn summary(&self) -> String {
match self {
Cmd::CallModel { turn, request } => format!(
"call_model(turn={}, model={}, msgs={})",
turn,
request.model_id,
request.messages.len()
),
Cmd::CompactConversation { turn, request } => format!(
"compact_conversation(turn={}, model={}, trigger={}, msgs={})",
turn,
request.chat.model_id,
request.trigger.as_str(),
request.chat.messages.len()
),
Cmd::ExecuteTool {
turn,
call_id,
source,
model_id: _,
} => format!(
"execute_tool(turn={}, call={}, fn={})",
turn, call_id, source.function.name
),
Cmd::CancelScope(turn) => format!("cancel_scope(turn={})", turn),
Cmd::SaveConversation(c) => format!("save_conversation(id={})", c.id),
Cmd::SaveCompactionArchive(a) => format!(
"save_compaction_archive(conversation={}, id={})",
a.conversation_id, a.id
),
Cmd::PersistLastModel(m) => format!("persist_last_model({})", m),
Cmd::PersistReasoningFor { model_id, level } => {
format!("persist_reasoning_for({}, {:?})", model_id, level)
},
Cmd::RefreshInstructions => "refresh_instructions".to_string(),
Cmd::LoadConversation(id) => format!("load_conversation({})", id),
Cmd::ListConversations => "list_conversations".to_string(),
Cmd::InitMcpServers(m) => format!("init_mcp_servers(n={})", m.len()),
Cmd::StopMcpServer { name } => format!("stop_mcp_server({})", name),
Cmd::PullOllamaModel { model } => format!("pull_ollama_model({})", model),
Cmd::OpenInSystem(p) => format!("open_in_system({})", p.display()),
Cmd::DismissStatusAfter { ms } => format!("dismiss_status_after({}ms)", ms),
Cmd::WriteImageToTemp {
path,
format,
bytes,
} => format!(
"write_image_to_temp(path={}, fmt={}, n={})",
path.display(),
format,
bytes.len()
),
Cmd::ReadClipboard => "read_clipboard".to_string(),
Cmd::Exit => "exit".to_string(),
Cmd::SetTerminalTitle(t) => format!("set_terminal_title({})", t),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn turn_scoped_variants_marked_correctly() {
let request = ChatRequest {
model_id: "m".to_string(),
messages: vec![],
system_prompt: String::new(),
instructions: None,
reasoning: ReasoningLevel::Medium,
temperature: 0.7,
max_tokens: 4096,
tools: vec![],
};
assert!(
Cmd::CallModel {
turn: TurnId(1),
request,
}
.is_turn_scoped()
);
assert!(
!Cmd::SaveConversation(ConversationHistory::new("/p".to_string(), "m".to_string()))
.is_turn_scoped()
);
assert!(!Cmd::RefreshInstructions.is_turn_scoped());
assert!(!Cmd::Exit.is_turn_scoped());
}
#[test]
fn cmd_tags_are_stable() {
assert_eq!(Cmd::Exit.tag(), "exit");
assert_eq!(Cmd::RefreshInstructions.tag(), "refresh_instructions");
assert_eq!(Cmd::CancelScope(TurnId(1)).tag(), "cancel_scope");
}
#[test]
fn cmd_summary_includes_identifying_fields() {
let c = Cmd::CancelScope(TurnId(42));
let s = c.summary();
assert!(s.contains("turn#42"));
}
}