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::runtime::{SafetyMode, TaskStatus};
use crate::session::ConversationHistory;
use super::state::ApprovalChoice;
use super::compaction::{CompactionArchive, CompactionRecord, CompactionRequest};
use super::ids::{ToolCallId, TurnId};
use super::runtime::ManagedProcess;
#[allow(clippy::large_enum_variant)]
#[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,
safety_mode: SafetyMode,
intent: Option<String>,
},
CancelScope(TurnId),
BackgroundScope(TurnId),
ResolveApproval {
call_id: ToolCallId,
decision: ApprovalChoice,
},
SaveConversation(ConversationHistory),
SaveCompactionArchive {
archive: CompactionArchive,
record: CompactionRecord,
conversation: ConversationHistory,
},
SaveProcess(ManagedProcess),
PersistLastModel(String),
PersistReasoningFor {
model_id: String,
level: ReasoningLevel,
},
PersistOllamaNumCtxFor {
model_id: String,
num_ctx: Option<u32>,
},
PersistOllamaOffload(bool),
RefreshInstructions,
RefreshMemory,
ListMemory,
RememberMemory { text: String },
ForgetMemory { id: String },
ConsolidateMemory { model_id: String },
LoadConversation(String),
ListConversations,
ListRuntimeTasks { limit: usize },
LoadRuntimeTask { id: String },
ListRuntimeProcesses { limit: usize },
ShowRuntimeProcessLogs { id: String },
StopRuntimeProcess { id: String },
RestartRuntimeProcess { id: String },
OpenRuntimeTarget { target: String },
ShowRuntimePorts,
ListRuntimeApprovals,
DecideRuntimeApproval { id: String, decision: String },
ListRuntimeCheckpoints { limit: usize },
ListRuntimePlugins,
UpdateRuntimeTaskStatus {
id: String,
status: TaskStatus,
final_report: Option<String>,
},
CreateRuntimeCheckpoint { paths: Vec<PathBuf> },
RestoreRuntimeCheckpoint { id: String },
ShowRuntimeModelInfo { model: String },
InitMcpServers(HashMap<String, McpServerConfig>),
StopMcpServer { name: String },
PullOllamaModel { model: String },
OpenInSystem(PathBuf),
DismissStatusAfter { ms: u64 },
WriteImageToTemp {
path: PathBuf,
bytes: Vec<u8>,
format: String,
},
ReadClipboard,
CopyToClipboard(String),
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>,
pub ollama_num_ctx: Option<u32>,
pub ollama_allow_ram_offload: Option<bool>,
}
#[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::BackgroundScope(_) => "background_scope",
Cmd::ResolveApproval { .. } => "resolve_approval",
Cmd::SaveConversation(_) => "save_conversation",
Cmd::SaveCompactionArchive { .. } => "save_compaction_archive",
Cmd::SaveProcess(_) => "save_process",
Cmd::PersistLastModel(_) => "persist_last_model",
Cmd::PersistReasoningFor { .. } => "persist_reasoning_for",
Cmd::PersistOllamaNumCtxFor { .. } => "persist_ollama_num_ctx_for",
Cmd::PersistOllamaOffload(_) => "persist_ollama_offload",
Cmd::RefreshInstructions => "refresh_instructions",
Cmd::RefreshMemory => "refresh_memory",
Cmd::ListMemory => "list_memory",
Cmd::RememberMemory { .. } => "remember_memory",
Cmd::ForgetMemory { .. } => "forget_memory",
Cmd::ConsolidateMemory { .. } => "consolidate_memory",
Cmd::LoadConversation(_) => "load_conversation",
Cmd::ListConversations => "list_conversations",
Cmd::ListRuntimeTasks { .. } => "list_runtime_tasks",
Cmd::LoadRuntimeTask { .. } => "load_runtime_task",
Cmd::ListRuntimeProcesses { .. } => "list_runtime_processes",
Cmd::ShowRuntimeProcessLogs { .. } => "show_runtime_process_logs",
Cmd::StopRuntimeProcess { .. } => "stop_runtime_process",
Cmd::RestartRuntimeProcess { .. } => "restart_runtime_process",
Cmd::OpenRuntimeTarget { .. } => "open_runtime_target",
Cmd::ShowRuntimePorts => "show_runtime_ports",
Cmd::ListRuntimeApprovals => "list_runtime_approvals",
Cmd::DecideRuntimeApproval { .. } => "decide_runtime_approval",
Cmd::ListRuntimeCheckpoints { .. } => "list_runtime_checkpoints",
Cmd::ListRuntimePlugins => "list_runtime_plugins",
Cmd::UpdateRuntimeTaskStatus { .. } => "update_runtime_task_status",
Cmd::CreateRuntimeCheckpoint { .. } => "create_runtime_checkpoint",
Cmd::RestoreRuntimeCheckpoint { .. } => "restore_runtime_checkpoint",
Cmd::ShowRuntimeModelInfo { .. } => "show_runtime_model_info",
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::CopyToClipboard(_) => "copy_to_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,
..
} => format!(
"execute_tool(turn={}, call={}, fn={})",
turn, call_id, source.function.name
),
Cmd::CancelScope(turn) => format!("cancel_scope(turn={})", turn),
Cmd::BackgroundScope(turn) => format!("background_scope(turn={})", turn),
Cmd::ResolveApproval { call_id, decision } => {
format!("resolve_approval(call={}, {:?})", call_id, decision)
},
Cmd::SaveConversation(c) => format!("save_conversation(id={})", c.id),
Cmd::SaveCompactionArchive {
archive, record, ..
} => format!(
"save_compaction_archive(conversation={}, id={})",
archive.conversation_id, record.id
),
Cmd::SaveProcess(p) => format!("save_process(id={}, pid={})", p.id, p.pid),
Cmd::PersistLastModel(m) => format!("persist_last_model({})", m),
Cmd::PersistReasoningFor { model_id, level } => {
format!("persist_reasoning_for({}, {:?})", model_id, level)
},
Cmd::PersistOllamaNumCtxFor { model_id, num_ctx } => {
format!("persist_ollama_num_ctx_for({}, {:?})", model_id, num_ctx)
},
Cmd::PersistOllamaOffload(enabled) => {
format!("persist_ollama_offload({})", enabled)
},
Cmd::RefreshInstructions => "refresh_instructions".to_string(),
Cmd::RefreshMemory => "refresh_memory".to_string(),
Cmd::ListMemory => "list_memory".to_string(),
Cmd::RememberMemory { .. } => "remember_memory".to_string(),
Cmd::ForgetMemory { .. } => "forget_memory".to_string(),
Cmd::ConsolidateMemory { .. } => "consolidate_memory".to_string(),
Cmd::LoadConversation(id) => format!("load_conversation({})", id),
Cmd::ListConversations => "list_conversations".to_string(),
Cmd::ListRuntimeTasks { limit } => format!("list_runtime_tasks(limit={})", limit),
Cmd::LoadRuntimeTask { id } => format!("load_runtime_task({})", id),
Cmd::ListRuntimeProcesses { limit } => {
format!("list_runtime_processes(limit={})", limit)
},
Cmd::ShowRuntimeProcessLogs { id } => format!("show_runtime_process_logs({})", id),
Cmd::StopRuntimeProcess { id } => format!("stop_runtime_process({})", id),
Cmd::RestartRuntimeProcess { id } => format!("restart_runtime_process({})", id),
Cmd::OpenRuntimeTarget { target } => format!("open_runtime_target({})", target),
Cmd::ShowRuntimePorts => "show_runtime_ports".to_string(),
Cmd::ListRuntimeApprovals => "list_runtime_approvals".to_string(),
Cmd::DecideRuntimeApproval { id, decision } => {
format!("decide_runtime_approval({}, {})", id, decision)
},
Cmd::ListRuntimeCheckpoints { limit } => {
format!("list_runtime_checkpoints(limit={})", limit)
},
Cmd::ListRuntimePlugins => "list_runtime_plugins".to_string(),
Cmd::UpdateRuntimeTaskStatus { id, status, .. } => {
format!("update_runtime_task_status({}, {})", id, status)
},
Cmd::CreateRuntimeCheckpoint { paths } => {
format!("create_runtime_checkpoint(n={})", paths.len())
},
Cmd::RestoreRuntimeCheckpoint { id } => format!("restore_runtime_checkpoint({})", id),
Cmd::ShowRuntimeModelInfo { model } => format!("show_runtime_model_info({})", model),
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::CopyToClipboard(t) => format!("copy_to_clipboard(n={})", t.chars().count()),
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![],
ollama_num_ctx: None,
ollama_allow_ram_offload: None,
};
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"));
}
}