Skip to main content

tycode_core/chat/
commands.rs

1use crate::ai::model::{Model, ModelCost};
2use crate::ai::{
3    Content, Message, MessageRole, ModelSettings, ReasoningBudget, TokenUsage, ToolUseData,
4};
5use crate::chat::actor::{create_provider, resume_session, TimingStat};
6use crate::chat::request::select_model_for_agent;
7use crate::chat::tools::{current_agent, current_agent_mut};
8use crate::chat::{
9    actor::ActorState,
10    events::{
11        ChatEvent, ChatMessage, MessageSender, ModelInfo, ToolExecutionResult, ToolRequest,
12        ToolRequestType,
13    },
14};
15
16use crate::module::{ContextComponentSelection, Module, SlashCommand};
17use crate::settings::config::{ProviderConfig, ReviewLevel};
18use chrono::Utc;
19use dirs;
20use serde_json::json;
21use std::collections::HashMap;
22use std::fs;
23use std::iter::Peekable;
24use std::str::Chars;
25use std::sync::Arc;
26use toml;
27
28use crate::persistence::storage;
29
30fn handle_escape_sequence(chars: &mut Peekable<Chars>, current: &mut String, c: char) {
31    let Some(&next) = chars.peek() else {
32        current.push(c);
33        return;
34    };
35    if next == '"' || next == '\\' {
36        chars.next();
37        current.push(next);
38        return;
39    }
40    current.push(c);
41}
42
43fn parse_command_with_quotes(input: &str) -> Vec<String> {
44    let mut parts = Vec::new();
45    let mut current = String::new();
46    let mut in_quotes = false;
47    let mut chars = input.chars().peekable();
48
49    while let Some(c) = chars.next() {
50        match c {
51            '"' => {
52                in_quotes = !in_quotes;
53            }
54            ' ' | '\t' if !in_quotes => {
55                if current.is_empty() {
56                    continue;
57                }
58                parts.push(current.clone());
59                current.clear();
60            }
61            '\\' if in_quotes => {
62                handle_escape_sequence(&mut chars, &mut current, c);
63            }
64            _ => {
65                current.push(c);
66            }
67        }
68    }
69
70    if !current.is_empty() {
71        parts.push(current);
72    }
73
74    parts
75}
76
77#[derive(Clone, Debug)]
78pub struct CommandInfo {
79    pub name: String,
80    pub description: String,
81    pub usage: String,
82    pub hidden: bool,
83}
84
85/// Process a command and directly mutate the actor state
86pub async fn process_command(state: &mut ActorState, command: &str) -> Vec<ChatMessage> {
87    let parts = parse_command_with_quotes(command);
88    if parts.is_empty() {
89        return vec![];
90    }
91
92    let command_name = parts[0].strip_prefix('/').unwrap_or(&parts[0]);
93    let parts_refs: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
94    let args = &parts_refs[1..];
95
96    let module_commands: Vec<Arc<dyn SlashCommand>> = state
97        .modules
98        .iter()
99        .flat_map(|m| m.slash_commands())
100        .collect();
101
102    if let Some(cmd) = module_commands.iter().find(|c| c.name() == command_name) {
103        return cmd.execute(state, &args).await;
104    }
105
106    match command_name {
107        "clear" => handle_clear_command(state).await,
108        "context" => handle_context_command(state).await,
109        "model" => handle_model_command(state, &parts_refs).await,
110        "settings" => handle_settings_command(state, &parts_refs).await,
111
112        "agentmodel" => handle_agentmodel_command(state, &parts_refs).await,
113        "agent" => handle_agent_command(state, &parts_refs).await,
114        "review_level" => handle_review_level_command(state, &parts_refs).await,
115        "cost" => handle_cost_command_with_subcommands(state, &parts_refs).await,
116
117        "help" => handle_help_command(&state.modules).await,
118        "models" => handle_models_command(state).await,
119        "provider" => handle_provider_command(state, &parts_refs).await,
120        "profile" => handle_profile_command(state, &parts_refs).await,
121        "sessions" => handle_sessions_command(state, &parts_refs).await,
122        "debug_ui" => handle_debug_ui_command(state).await,
123        _ => vec![create_message(
124            format!("Unknown command: /{}", command_name),
125            MessageSender::Error,
126        )],
127    }
128}
129
130/// Check if the given input string starts with a known command
131pub fn is_known_command(input: &str, modules: &[Arc<dyn Module>]) -> bool {
132    let first_word = input.split_whitespace().next().unwrap_or("");
133    let command_name = first_word.strip_prefix('/').unwrap_or(first_word);
134
135    let commands = get_core_commands();
136    if commands.iter().any(|cmd| cmd.name == command_name) {
137        return true;
138    }
139
140    for module in modules {
141        for cmd in module.slash_commands() {
142            if cmd.name() == command_name {
143                return true;
144            }
145        }
146    }
147
148    false
149}
150
151/// Get core commands (not from modules)
152fn get_core_commands() -> Vec<CommandInfo> {
153    vec![
154        CommandInfo {
155            name: "clear".to_string(),
156            description: r"Clear the conversation history".to_string(),
157            usage: "/clear".to_string(),
158            hidden: false,
159        },
160        CommandInfo {
161            name: "context".to_string(),
162            description: r"Show what files would be included in the AI context".to_string(),
163            usage: "/context".to_string(),
164            hidden: false,
165        },
166        CommandInfo {
167            name: r"model".to_string(),
168            description: r"Set the AI model for all agents".to_string(),
169            usage: r"/model <name> [temperature=0.7] [max_tokens=4096] [top_p=1.0] [reasoning_budget=...]".to_string(),
170            hidden: false,
171        },
172        CommandInfo {
173            name: "trace".to_string(),
174            description: r"Enable/disable trace logging to .tycode/trace".to_string(),
175            usage: "/trace <on|off>".to_string(),
176            hidden: false,
177        },
178        CommandInfo {
179            name: "settings".to_string(),
180            description: "Display current settings and configuration".to_string(),
181            usage: "/settings or /settings save".to_string(),
182            hidden: false,
183        },
184
185        CommandInfo {
186            name: "cost".to_string(),
187            description: "Show session token usage and estimated cost, or set model cost limit".to_string(),
188            usage: "/cost [set <free|low|medium|high|unlimited>]".to_string(),
189            hidden: false,
190        },
191        CommandInfo {
192            name: "help".to_string(),
193            description: "Show this help message".to_string(),
194            usage: "/help".to_string(),
195            hidden: false,
196        },
197        CommandInfo {
198            name: "models".to_string(),
199            description: "List available AI models".to_string(),
200            usage: "/models".to_string(),
201            hidden: false,
202        },
203        CommandInfo {
204            name: "provider".to_string(),
205            description: "List, switch, or add AI providers".to_string(),
206            usage: "/provider [name] | /provider add <name> <type> [args]".to_string(),
207            hidden: false,
208        },
209        CommandInfo {
210            name: "agentmodel".to_string(),
211            description: "Set the AI model for a specific agent with tunings".to_string(),
212            usage: "/agentmodel <agent_name> <model_name> [temperature=0.7] [max_tokens=4096] [top_p=1.0] [reasoning_budget=...]".to_string(),
213            hidden: false,
214        },
215        CommandInfo {
216            name: "agent".to_string(),
217            description: "Switch the current agent".to_string(),
218            usage: "/agent <name>".to_string(),
219            hidden: false,
220        },
221        CommandInfo {
222            name: "review_level".to_string(),
223            description: "Set the review level (None, Task)".to_string(),
224            usage: "/review_level <none|task>".to_string(),
225            hidden: false,
226        },
227
228        CommandInfo {
229            name: "quit".to_string(),
230            description: "Exit the application".to_string(),
231            usage: "/quit or /exit".to_string(),
232            hidden: false,
233        },
234        CommandInfo {
235            name: "profile".to_string(),
236            description: "Manage settings profiles (switch, save, list, show current)".to_string(),
237            usage: "/profile [switch|save|list|show] [<name>]".to_string(),
238            hidden: false,
239        },
240        CommandInfo {
241            name: "sessions".to_string(),
242            description: "Manage conversation sessions (list, resume, delete, gc)".to_string(),
243            usage: "/sessions [list|resume <id>|delete <id>|gc [days]]".to_string(),
244            hidden: false,
245        },
246        CommandInfo {
247            name: "debug_ui".to_string(),
248            description: "Internal: Test UI components without AI calls".to_string(),
249            usage: "/debug_ui".to_string(),
250            hidden: true,
251        },
252    ]
253}
254
255/// Get all available commands with their descriptions
256pub fn get_available_commands(modules: &[Arc<dyn Module>]) -> Vec<CommandInfo> {
257    let mut commands = get_core_commands();
258
259    for module in modules {
260        for cmd in module.slash_commands() {
261            commands.push(CommandInfo {
262                name: cmd.name().to_string(),
263                description: cmd.description().to_string(),
264                usage: cmd.usage().to_string(),
265                hidden: cmd.hidden(),
266            });
267        }
268    }
269
270    commands
271}
272
273async fn handle_clear_command(state: &mut ActorState) -> Vec<ChatMessage> {
274    state.clear_conversation();
275    current_agent_mut(state, |a| a.conversation.clear());
276    vec![create_message(
277        "Conversation cleared.".to_string(),
278        MessageSender::System,
279    )]
280}
281
282async fn handle_context_command(state: &ActorState) -> Vec<ChatMessage> {
283    let context_content = state
284        .context_builder
285        .build(&ContextComponentSelection::All, &state.modules)
286        .await;
287
288    let message = if context_content.is_empty() {
289        "=== Current Context ===\n\nNo context components configured.".to_string()
290    } else {
291        format!("=== Current Context ===\n{}", context_content)
292    };
293
294    vec![create_message(message, MessageSender::System)]
295}
296
297async fn handle_settings_command(state: &ActorState, parts: &[&str]) -> Vec<ChatMessage> {
298    let settings = state.settings.settings();
299
300    if parts.is_empty() || (parts.len() == 1) {
301        let content = match toml::to_string_pretty(&settings) {
302            Ok(c) => c,
303            Err(e) => {
304                return vec![create_message(
305                    format!("Failed to serialize settings: {}", e),
306                    MessageSender::Error,
307                )];
308            }
309        };
310
311        let message = format!("=== Current Settings ===\n\n{}", content);
312
313        vec![create_message(message, MessageSender::System)]
314    } else if parts.len() > 1 && parts[1] == "save" {
315        match state.settings.save() {
316            Ok(()) => vec![create_message(
317                "Settings saved to disk successfully.".to_string(),
318                MessageSender::System,
319            )],
320            Err(e) => vec![create_message(
321                format!("Failed to save settings: {e}"),
322                MessageSender::Error,
323            )],
324        }
325    } else {
326        vec![create_message(
327            format!("Unknown arguments: {parts:?}"),
328            MessageSender::Error,
329        )]
330    }
331}
332
333async fn handle_cost_command_with_subcommands(
334    state: &mut ActorState,
335    parts: &[&str],
336) -> Vec<ChatMessage> {
337    if parts.len() >= 3 && parts[1] == "set" {
338        let level_str = parts[2];
339        let new_level = match ModelCost::try_from(level_str) {
340            Ok(level) => level,
341            Err(e) => {
342                return vec![create_message(
343                    format!("Invalid cost level. {}", e),
344                    MessageSender::Error,
345                )];
346            }
347        };
348
349        state
350            .settings
351            .update_setting(|s| s.model_quality = Some(new_level));
352
353        return vec![create_message(
354            format!("Model cost level set to: {:?}.\n\nSettings updated for this session. Call `/settings save` to use these settings as default for all future sessions.", new_level),
355            MessageSender::System,
356        )];
357    } else if parts.len() >= 2 && parts[1] == "set" {
358        // Insufficient args for set
359        return vec![create_message(
360            "Usage: /cost set <free|low|medium|high|unlimited>".to_string(),
361            MessageSender::Error,
362        )];
363    }
364
365    // Default: show cost summary
366    handle_cost_command(&state).await
367}
368
369async fn handle_cost_command(state: &ActorState) -> Vec<ChatMessage> {
370    let usage = &state.session_token_usage;
371    let agent_name = current_agent(state, |a| a.agent.name().to_string());
372    let settings_snapshot = state.settings.settings();
373    let model_settings =
374        select_model_for_agent(&settings_snapshot, state.provider.as_ref(), &agent_name)
375            .unwrap_or_else(|_| Model::None.default_settings());
376    let current_model = model_settings.model;
377    let total_input_tokens = usage.input_tokens + usage.cache_creation_input_tokens.unwrap_or(0);
378
379    let mut message = String::new();
380    message.push_str("=== Session Cost Summary ===\n\n");
381    message.push_str(&format!("Current Model: {current_model:?}\n"));
382    message.push_str(&format!("Provider: {}\n\n", state.provider.name()));
383
384    message.push_str("Token Usage:\n");
385    message.push_str(&format!("  Input tokens:  {:>8}\n", total_input_tokens));
386    message.push_str(&format!("  Output tokens: {:>8}\n", usage.output_tokens));
387    message.push_str(&format!("  Total tokens:  {:>8}\n\n", usage.total_tokens));
388
389    // Add breakdown if there are cache-related tokens
390    if usage.cache_creation_input_tokens.unwrap_or(0) > 0
391        || usage.cached_prompt_tokens.unwrap_or(0) > 0
392    {
393        message.push_str("Token Breakdown:\n");
394        message.push_str(&format!(
395            "  Base input tokens:            {:>8}\n",
396            usage.input_tokens
397        ));
398        if let Some(cache_creation) = usage.cache_creation_input_tokens {
399            if cache_creation > 0 {
400                message.push_str(&format!(
401                    "  Cache creation input tokens:  {:>8}\n",
402                    cache_creation
403                ));
404            }
405        }
406        if let Some(cached) = usage.cached_prompt_tokens {
407            if cached > 0 {
408                message.push_str(&format!("  Cached prompt tokens:         {:>8}\n", cached));
409            }
410        }
411        message.push_str("\n");
412    }
413
414    message.push_str("Accumulated Cost:\n");
415    message.push_str(&format!("  Total cost: ${:.6}\n", state.session_cost));
416
417    if usage.total_tokens > 0 {
418        let avg_cost_per_1k = (state.session_cost / usage.total_tokens as f64) * 1000.0;
419        message.push_str(&format!("  Average per 1K tokens: ${avg_cost_per_1k:.6}\n"));
420    }
421
422    let TimingStat {
423        waiting_for_human,
424        ai_processing,
425        tool_execution,
426    } = state.timing_stats.session();
427    let total_time = waiting_for_human + ai_processing + tool_execution;
428    message.push_str("\nTime Spent:\n");
429    message.push_str(&format!(
430        "  Waiting for human: {:>6.1}s\n",
431        waiting_for_human.as_secs_f64()
432    ));
433    message.push_str(&format!(
434        "  AI processing:     {:>6.1}s\n",
435        ai_processing.as_secs_f64()
436    ));
437    message.push_str(&format!(
438        "  Tool execution:    {:>6.1}s\n",
439        tool_execution.as_secs_f64()
440    ));
441    message.push_str(&format!(
442        "  Total session:     {:>6.1}s\n",
443        total_time.as_secs_f64()
444    ));
445
446    vec![create_message(message, MessageSender::System)]
447}
448
449async fn handle_help_command(modules: &[Arc<dyn Module>]) -> Vec<ChatMessage> {
450    let commands = get_available_commands(modules);
451    let mut message = String::from("Available commands:\n\n");
452
453    for cmd in commands {
454        if !cmd.hidden {
455            message.push_str(&format!("/{} - {}\n", cmd.name, cmd.description));
456            message.push_str(&format!("  Usage: {}\n\n", cmd.usage));
457        }
458    }
459    vec![create_message(message, MessageSender::System)]
460}
461
462async fn handle_models_command(state: &ActorState) -> Vec<ChatMessage> {
463    let models = state.provider.supported_models();
464    let model_names: Vec<String> = if models.is_empty() {
465        vec![Model::GrokCodeFast1.name().to_string()]
466    } else {
467        models.iter().map(|m| m.name().to_string()).collect()
468    };
469    let response = model_names.join(", ");
470    vec![create_message(response, MessageSender::System)]
471}
472
473async fn handle_model_command(state: &mut ActorState, parts: &[&str]) -> Vec<ChatMessage> {
474    if parts.len() < 2 {
475        return vec![create_message(
476            "Usage: /model <name> [key=value...]\nValid keys: temperature, max_tokens, top_p, reasoning_budget\nUse /models to list available models.".to_string(),
477            MessageSender::System,
478        )];
479    }
480
481    let model_name = parts[1];
482    let model = match Model::from_name(model_name) {
483        Some(m) => m,
484        None => {
485            return vec![create_message(
486                format!("Unknown model: {model_name}. Use /models to list available models."),
487                MessageSender::Error,
488            )];
489        }
490    };
491
492    let settings = match parse_model_settings_overrides(&model, &parts[2..]) {
493        Ok(s) => s,
494        Err(e) => return vec![create_message(e, MessageSender::Error)],
495    };
496
497    // Set for all agents
498    let agent_names: Vec<String> = state.agent_catalog.get_agent_names();
499    for agent_name in agent_names {
500        state
501            .settings
502            .update_setting(|s| s.set_agent_model(agent_name, settings.clone()));
503    }
504
505    // Success message
506    let mut overrides = Vec::new();
507    if settings.temperature.is_some() {
508        overrides.push(format!("temperature={}", settings.temperature.unwrap()));
509    }
510    if settings.max_tokens.is_some() {
511        overrides.push(format!("max_tokens={}", settings.max_tokens.unwrap()));
512    }
513    if settings.top_p.is_some() {
514        overrides.push(format!("top_p={}", settings.top_p.unwrap()));
515    }
516    overrides.push(format!("reasoning_budget={}", settings.reasoning_budget));
517
518    let overrides_str = if overrides.is_empty() {
519        "".to_string()
520    } else {
521        format!(" (with {})", overrides.join(", "))
522    };
523
524    vec![create_message(
525        format!(
526            "Model successfully set to {} for all agents{}.\n\nSettings updated for this session. Call `/settings save` to use these settings as default for all future sessions.",
527            model.name(),
528            overrides_str
529        ),
530        MessageSender::System,
531    )]
532}
533
534async fn handle_agentmodel_command(state: &mut ActorState, parts: &[&str]) -> Vec<ChatMessage> {
535    if parts.len() < 3 {
536        return vec![create_message(format!("Usage: /agentmodel <agent_name> <model_name> [temperature=0.7] [max_tokens=4096] [top_p=1.0] [reasoning_budget=...]\nValid agents: {}", state.agent_catalog.get_agent_names().join(", ")), MessageSender::System)];
537    }
538    let agent_name = parts[1];
539    if !state
540        .agent_catalog
541        .get_agent_names()
542        .contains(&agent_name.to_string())
543    {
544        return vec![create_message(
545            format!(
546                "Unknown agent: {}. Valid agents: {}",
547                agent_name,
548                state.agent_catalog.get_agent_names().join(", ")
549            ),
550            MessageSender::Error,
551        )];
552    }
553    let model_name = parts[2];
554    let model = match Model::from_name(model_name) {
555        Some(m) => m,
556        None => {
557            return vec![create_message(
558                format!("Unknown model: {model_name}. Use /models to list available models."),
559                MessageSender::Error,
560            )]
561        }
562    };
563    let settings = match parse_model_settings_overrides(&model, &parts[3..]) {
564        Ok(s) => s,
565        Err(e) => return vec![create_message(e, MessageSender::Error)],
566    };
567    state
568        .settings
569        .update_setting(|s| s.set_agent_model(agent_name.to_string(), settings.clone()));
570    // Collect overrides for message
571    let mut overrides = Vec::new();
572    if let Some(v) = settings.temperature {
573        overrides.push(format!("temperature={v}"));
574    }
575    if let Some(v) = settings.max_tokens {
576        overrides.push(format!("max_tokens={v}"));
577    }
578    if let Some(v) = settings.top_p {
579        overrides.push(format!("top_p={v}"));
580    }
581    overrides.push(format!("reasoning_budget={}", settings.reasoning_budget));
582
583    let overrides_str = if overrides.is_empty() {
584        "".to_string()
585    } else {
586        format!(" (with {})", overrides.join(", "))
587    };
588    vec![create_message(
589        format!(
590            "Model successfully set to {} for agent {}{}.\n\nSettings updated for this session. Call `/settings save` to use these settings as default for all future sessions.",
591            model.name(),
592            agent_name,
593            overrides_str
594        ),
595        MessageSender::System,
596    )]
597}
598
599fn parse_model_settings_overrides(
600    model: &Model,
601    overrides: &[&str],
602) -> Result<ModelSettings, String> {
603    let mut settings = model.default_settings();
604    for &arg in overrides {
605        let eq_pos = arg
606            .find('=')
607            .ok_or(format!("Invalid argument: {arg}. Expected key=value"))?;
608        let key = &arg[..eq_pos];
609        let value_str = &arg[eq_pos + 1..];
610        match key {
611            "temperature" => {
612                let v: f32 = value_str.parse().map_err(|_| format!("Invalid temperature value: {value_str}. Expected a float (e.g., 0.7)."))?;
613                settings.temperature = Some(v);
614            }
615            "max_tokens" => {
616                let v: u32 = value_str.parse().map_err(|_| format!("Invalid max_tokens value: {value_str}. Expected a positive integer (e.g., 4096)."))?;
617                settings.max_tokens = Some(v);
618            }
619            "top_p" => {
620                let v: f32 = value_str.parse().map_err(|_| format!("Invalid top_p value: {value_str}. Expected a float (e.g., 1.0)."))?;
621                settings.top_p = Some(v);
622            }
623            "reasoning_budget" => {
624                let reasoning_budget = match value_str {
625                    "High" | "high" => ReasoningBudget::High,
626                    "Low" | "low" => ReasoningBudget::Low,
627                    "Off" | "off" => ReasoningBudget::Off,
628                    _ => return Err("Unsupported reasoning budget - must be one of high low or off".to_string())
629                };
630                settings.reasoning_budget = reasoning_budget;
631            }
632            _ => return Err(format!("Unknown parameter: {key}. Valid parameters: temperature, max_tokens, top_p, reasoning_budget")),
633        }
634    }
635    Ok(settings)
636}
637
638fn create_message(content: String, sender: MessageSender) -> ChatMessage {
639    ChatMessage {
640        content,
641        sender,
642        timestamp: Utc::now().timestamp_millis() as u64,
643        reasoning: None,
644        tool_calls: Vec::new(),
645        model_info: None,
646        token_usage: None,
647        images: vec![],
648    }
649}
650
651async fn handle_agent_command(state: &mut ActorState, parts: &[&str]) -> Vec<ChatMessage> {
652    if parts.len() < 2 {
653        return vec![create_message(
654            format!(
655                "Usage: /agent <name>. Valid agents: {}",
656                state.agent_catalog.get_agent_names().join(", ")
657            ),
658            MessageSender::System,
659        )];
660    }
661
662    let agent_name = parts[1];
663
664    if !state
665        .agent_catalog
666        .get_agent_names()
667        .contains(&agent_name.to_string())
668    {
669        return vec![create_message(
670            format!(
671                "Unknown agent: {}. Valid agents: {}",
672                agent_name,
673                state.agent_catalog.get_agent_names().join(", ")
674            ),
675            MessageSender::System,
676        )];
677    }
678
679    let had_sub_agents = state.spawn_module.stack_depth() > 1;
680
681    let current_name = current_agent(state, |a| a.agent.name().to_string());
682    if state.spawn_module.stack_depth() == 1 && current_name == agent_name {
683        return vec![create_message(
684            format!("Already switched to agent: {agent_name}"),
685            MessageSender::System,
686        )];
687    }
688
689    let merged_conversation = state
690        .spawn_module
691        .with_agents(|agents| {
692            let mut merged = Vec::new();
693            let agent_count = agents.len();
694            for (index, active_agent) in agents.iter().enumerate() {
695                merged.extend(active_agent.conversation.clone());
696
697                // Add delimiter between agent conversations to preserve context awareness
698                if agent_count > 1 && index < agent_count - 1 {
699                    merged.push(Message {
700                        role: MessageRole::Assistant,
701                        content: Content::text_only(format!(
702                            "[Context transition: The above is from the {} agent. Sub-agent context follows. All prior conversation history remains relevant.]",
703                            active_agent.agent.name()
704                        )),
705                    });
706                }
707            }
708            merged
709        })
710        .unwrap_or_default();
711
712    let new_agent_dyn = state.agent_catalog.create_agent(agent_name).unwrap();
713    state.spawn_module.reset_to_agent(new_agent_dyn);
714    state
715        .spawn_module
716        .with_root_agent_mut(|a| a.conversation = merged_conversation);
717
718    let suffix = if had_sub_agents {
719        " (sub-agent conversations merged)"
720    } else {
721        ""
722    };
723
724    vec![create_message(
725        format!("Switched to agent: {agent_name}{suffix}"),
726        MessageSender::System,
727    )]
728}
729
730async fn handle_review_level_command(state: &mut ActorState, parts: &[&str]) -> Vec<ChatMessage> {
731    if parts.len() < 2 {
732        // Show current review level
733        let current_level = &state.settings.settings().review_level;
734        return vec![create_message(
735            format!("Current review level: {current_level:?}"),
736            MessageSender::System,
737        )];
738    }
739
740    // Parse the review level from the command
741    let level_str = parts[1].to_lowercase();
742    let new_level = match level_str.as_str() {
743        "none" => ReviewLevel::None,
744        "task" => ReviewLevel::Task,
745        _ => {
746            return vec![create_message(
747                "Invalid review level. Valid options: none, task".to_string(),
748                MessageSender::Error,
749            )]
750        }
751    };
752
753    // Update the setting
754    state
755        .settings
756        .update_setting(|s| s.review_level = new_level.clone());
757
758    vec![create_message(
759        format!("Review level set to: {:?}.\n\nSettings updated for this session. Call `/settings save` to use these settings as default for all future sessions.", new_level),
760        MessageSender::System,
761    )]
762}
763
764async fn handle_provider_command(state: &mut ActorState, parts: &[&str]) -> Vec<ChatMessage> {
765    if parts.len() < 2 {
766        let settings = state.settings.settings();
767        let providers = settings.list_providers();
768        let current_provider = state.provider.name();
769
770        let mut message = String::new();
771        message.push_str("Available providers:\n\n");
772
773        for provider in providers {
774            if provider == current_provider {
775                message.push_str(&format!("  {provider} (active)\n"));
776            } else {
777                message.push_str(&format!("  {provider}\n"));
778            }
779        }
780
781        return vec![create_message(message, MessageSender::System)];
782    }
783
784    if parts[1].eq_ignore_ascii_case("add") {
785        return handle_provider_add_command(state, parts).await;
786    }
787
788    let provider_name = parts[1];
789
790    // Create new provider instance
791    let new_provider = match create_provider(&state.settings, provider_name).await {
792        Ok(provider) => provider,
793        Err(e) => {
794            return vec![create_message(
795                format!("Failed to create provider '{provider_name}': {e}"),
796                MessageSender::Error,
797            )];
798        }
799    };
800
801    // Update the active provider in memory (but don't save to disk)
802    state.provider = new_provider;
803    state.settings.update_setting(|settings| {
804        settings.active_provider = Some(provider_name.to_string());
805    });
806
807    vec![create_message(
808        format!("Active provider changed to: {provider_name}"),
809        MessageSender::System,
810    )]
811}
812
813async fn handle_provider_add_command(state: &mut ActorState, parts: &[&str]) -> Vec<ChatMessage> {
814    if parts.len() < 4 {
815        return vec![create_message(
816            "Usage: /provider add <name> <bedrock|openrouter|claude_code> <args...>".to_string(),
817            MessageSender::System,
818        )];
819    }
820
821    let alias = parts[2].to_string();
822    let provider_type = parts[3].to_lowercase();
823
824    let provider_config = match provider_type.as_str() {
825        "bedrock" => {
826            if parts.len() < 5 {
827                return vec![create_message(
828                    "Usage: /provider add <name> bedrock <profile> [region]".to_string(),
829                    MessageSender::Error,
830                )];
831            }
832            let profile = parts[4].to_string();
833
834            let region = if parts.len() > 5 {
835                parts[5..].join(" ")
836            } else {
837                "us-west-2".to_string()
838            };
839
840            ProviderConfig::Bedrock { profile, region }
841        }
842        "openrouter" => {
843            let api_key = parts[4..].join(" ");
844            if api_key.is_empty() {
845                return vec![create_message(
846                    "OpenRouter provider requires an API key".to_string(),
847                    MessageSender::Error,
848                )];
849            }
850
851            ProviderConfig::OpenRouter { api_key }
852        }
853        "claude_code" => {
854            let command = if parts.len() > 4 {
855                parts[4].to_string()
856            } else {
857                "claude".to_string()
858            };
859            let extra_args = if parts.len() > 5 {
860                parts[5..].iter().map(|s| s.to_string()).collect()
861            } else {
862                Vec::new()
863            };
864
865            ProviderConfig::ClaudeCode {
866                command,
867                extra_args,
868                env: HashMap::new(),
869            }
870        }
871        other => {
872            return vec![create_message(
873                format!(
874                    "Unsupported provider type '{other}'. Supported types: bedrock, openrouter, claude_code"
875                ),
876                MessageSender::Error,
877            )]
878        }
879    };
880
881    let current_settings = state.settings.settings();
882    let replacing = current_settings.providers.contains_key(&alias);
883    let should_set_active = current_settings.active_provider.is_none();
884
885    state.settings.update_setting(|settings| {
886        settings.add_provider(alias.clone(), provider_config.clone());
887        if should_set_active {
888            settings.active_provider = Some(alias.clone());
889        }
890    });
891
892    if let Err(e) = state.settings.save() {
893        return vec![create_message(
894            format!("Provider updated for this session but failed to save settings: {e}"),
895            MessageSender::Error,
896        )];
897    }
898
899    let mut response = if replacing {
900        format!("Updated provider '{alias}' ({provider_type})")
901    } else {
902        format!("Added provider '{alias}' ({provider_type})")
903    };
904
905    let mut messages = Vec::new();
906
907    if should_set_active {
908        response.push_str(" and set as the active provider");
909
910        match create_provider(&state.settings, &alias).await {
911            Ok(provider) => {
912                state.provider = provider;
913            }
914            Err(e) => {
915                messages.push(create_message(
916                    format!("Failed to initialize provider '{alias}': {e}"),
917                    MessageSender::Error,
918                ));
919            }
920        }
921    }
922
923    response.push('.');
924
925    messages.insert(0, create_message(response, MessageSender::System));
926    messages
927}
928
929pub async fn handle_debug_ui_command(state: &mut ActorState) -> Vec<ChatMessage> {
930    state
931        .event_sender
932        .send_message(ChatMessage::system("System message".to_string()));
933
934    state
935        .event_sender
936        .send_message(ChatMessage::warning("Warning message".to_string()));
937
938    state
939        .event_sender
940        .send_message(ChatMessage::error("Error message".to_string()));
941
942    // Test Bug #1: Retry counter positioning
943    // Send multiple retry attempts with messages in between to test retry positioning
944    state.send_event_replay(ChatEvent::RetryAttempt {
945        attempt: 1,
946        max_retries: 3,
947        backoff_ms: 2000,
948        error: "Network timeout - testing retry counter positioning bug".to_string(),
949    });
950
951    // Add some messages between retries to simulate the bug condition
952    state.event_sender.send_message(ChatMessage::system(
953        "Test message added between retry attempts to verify retry counter stays at bottom"
954            .to_string(),
955    ));
956
957    state.send_event_replay(ChatEvent::RetryAttempt {
958        attempt: 2,
959        max_retries: 3,
960        backoff_ms: 4000,
961        error: "Connection refused - retry counter should move to bottom".to_string(),
962    });
963
964    // Test Bug #3: Agent spawning messages should appear before agent messages
965    state.event_sender.send_message(ChatMessage::system(
966        "🔄 Spawning agent for task: Testing UI bug fixes".to_string(),
967    ));
968
969    // Test Bug #2: View diff button with long file path
970    // Create tool calls including one with an extremely long file path
971    let tool_calls = vec![
972        ToolUseData {
973            id: "test_long_path_0".to_string(),
974            name: "function".to_string(),
975            arguments: json!({
976                "name": "modify_file",
977                "arguments": {
978                    "file_path": "/very/long/nested/directory/structure/that/goes/on/and/on/and/on/testing/view/diff/button/overflow/bug/with/extremely/long/file/path/names/that/should/not/push/button/off/screen/component/module/submodule/feature/implementation/details/config/settings/final_file.rs",
979                    "before": "// old code",
980                    "after": "// new code with fixes"
981                }
982            }),
983        },
984        ToolUseData {
985            id: "test_modify_1".to_string(),
986            name: "function".to_string(),
987            arguments: json!({
988                "name": "modify_file",
989                "arguments": {
990                    "file_path": "/example/normal_path.rs",
991                    "before": "fn old_function() {\n    println!(\"old\");\n}",
992                    "after": "fn new_function() {\n    println!(\"new\");\n    println!(\"improved\");\n}"
993                }
994            }),
995        },
996        ToolUseData {
997            id: "test_run_2".to_string(),
998            name: "function".to_string(),
999            arguments: json!({
1000                "name": "run_build_test",
1001                "arguments": {
1002                    "command": "echo Testing UI fixes",
1003                    "timeout_seconds": 30,
1004                    "working_directory": "/"
1005                }
1006            }),
1007        },
1008    ];
1009
1010    // Send assistant message with tool calls to simulate AI response
1011    state.event_sender.send_message(ChatMessage::assistant(
1012        "coder".to_string(),
1013        "Testing UI bug fixes:\n1. Retry counter positioning (should always be at bottom)\n2. View diff button with long file paths (should not overflow off-screen)".to_string(),
1014        tool_calls.clone(),
1015        ModelInfo {
1016            model: crate::ai::model::Model::GrokCodeFast1,
1017        },
1018        TokenUsage {
1019            input_tokens: 100,
1020            output_tokens: 200,
1021            total_tokens: 300,
1022            cached_prompt_tokens: None,
1023            reasoning_tokens: None,
1024            cache_creation_input_tokens: None,
1025        },
1026        None,
1027    ));
1028
1029    // Create mock tool requests
1030    let tool_requests = vec![
1031        ToolRequest {
1032            tool_call_id: "test_long_path_0".to_string(),
1033            tool_name: "modify_file".to_string(),
1034            tool_type: ToolRequestType::ModifyFile {
1035                file_path: "/very/long/nested/directory/structure/that/goes/on/and/on/and/on/testing/view/diff/button/overflow/bug/with/extremely/long/file/path/names/that/should/not/push/button/off/screen/component/module/submodule/feature/implementation/details/config/settings/final_file.rs".to_string(),
1036                before: "// old code".to_string(),
1037                after: "// new code with fixes".to_string(),
1038            },
1039        },
1040        ToolRequest {
1041            tool_call_id: "test_modify_1".to_string(),
1042            tool_name: "modify_file".to_string(),
1043            tool_type: ToolRequestType::ModifyFile {
1044                file_path: "/example/normal_path.rs".to_string(),
1045                before: "fn old_function() {\n    println!(\"old\");\n}".to_string(),
1046                after:
1047                    "fn new_function() {\n    println!(\"new\");\n    println!(\"improved\");\n}"
1048                        .to_string(),
1049            },
1050        },
1051        ToolRequest {
1052            tool_call_id: "test_run_2".to_string(),
1053            tool_name: "run_build_test".to_string(),
1054            tool_type: ToolRequestType::RunCommand {
1055                command: "echo Testing UI fixes".to_string(),
1056                working_directory: "/".to_string(),
1057            },
1058        },
1059    ];
1060
1061    // Send ToolRequest events
1062    for tool_request in &tool_requests {
1063        state.send_event_replay(ChatEvent::ToolRequest(tool_request.clone()));
1064    }
1065
1066    // Send successful ToolExecutionCompleted for long path (this will test the view diff button)
1067    state.send_event_replay(ChatEvent::ToolExecutionCompleted {
1068        tool_call_id: "test_long_path_0".to_string(),
1069        tool_name: "modify_file".to_string(),
1070        tool_result: ToolExecutionResult::ModifyFile {
1071            lines_added: 5,
1072            lines_removed: 1,
1073        },
1074        success: true,
1075        error: None,
1076    });
1077
1078    // Send successful ToolExecutionCompleted for normal path
1079    state.send_event_replay(ChatEvent::ToolExecutionCompleted {
1080        tool_call_id: "test_modify_1".to_string(),
1081        tool_name: "modify_file".to_string(),
1082        tool_result: ToolExecutionResult::ModifyFile {
1083            lines_added: 3,
1084            lines_removed: 2,
1085        },
1086        success: true,
1087        error: None,
1088    });
1089
1090    // Send successful ToolExecutionCompleted for command
1091    state.send_event_replay(ChatEvent::ToolExecutionCompleted {
1092        tool_call_id: "test_run_2".to_string(),
1093        tool_name: "run_build_test".to_string(),
1094        tool_result: ToolExecutionResult::RunCommand {
1095            exit_code: 0,
1096            stdout: "Testing UI fixes\n".to_string(),
1097            stderr: "".to_string(),
1098        },
1099        success: true,
1100        error: None,
1101    });
1102
1103    // Test SearchTypes and GetTypeDocs tool requests
1104    // First, send an assistant message with tool_calls to create the tool items in the UI
1105    let analyzer_tool_calls = vec![
1106        ToolUseData {
1107            id: "test_search_types".to_string(),
1108            name: "search_types".to_string(),
1109            arguments: json!({
1110                "type_name": "Config",
1111                "language": "rust",
1112                "workspace_root": "/example/project"
1113            }),
1114        },
1115        ToolUseData {
1116            id: "test_get_type_docs".to_string(),
1117            name: "get_type_docs".to_string(),
1118            arguments: json!({
1119                "type_path": "src/config.rs::Config",
1120                "language": "rust",
1121                "workspace_root": "/example/project"
1122            }),
1123        },
1124    ];
1125
1126    state.event_sender.send_message(ChatMessage::assistant(
1127        "coder".to_string(),
1128        "Testing analyzer tools: search_types and get_type_docs".to_string(),
1129        analyzer_tool_calls,
1130        ModelInfo {
1131            model: crate::ai::model::Model::GrokCodeFast1,
1132        },
1133        TokenUsage {
1134            input_tokens: 100,
1135            output_tokens: 50,
1136            total_tokens: 150,
1137            cached_prompt_tokens: None,
1138            reasoning_tokens: None,
1139            cache_creation_input_tokens: None,
1140        },
1141        None,
1142    ));
1143
1144    // Now send ToolRequest events to update the tool items
1145    state.send_event_replay(ChatEvent::ToolRequest(ToolRequest {
1146        tool_call_id: "test_search_types".to_string(),
1147        tool_name: "search_types".to_string(),
1148        tool_type: ToolRequestType::SearchTypes {
1149            language: "rust".to_string(),
1150            workspace_root: "/example/project".to_string(),
1151            type_name: "Config".to_string(),
1152        },
1153    }));
1154
1155    state.send_event_replay(ChatEvent::ToolExecutionCompleted {
1156        tool_call_id: "test_search_types".to_string(),
1157        tool_name: "search_types".to_string(),
1158        tool_result: ToolExecutionResult::SearchTypes {
1159            types: vec![
1160                "src/config.rs::Config".to_string(),
1161                "src/settings/mod.rs::Config".to_string(),
1162            ],
1163        },
1164        success: true,
1165        error: None,
1166    });
1167
1168    // Test GetTypeDocs tool request and completion
1169    state.send_event_replay(ChatEvent::ToolRequest(ToolRequest {
1170        tool_call_id: "test_get_type_docs".to_string(),
1171        tool_name: "get_type_docs".to_string(),
1172        tool_type: ToolRequestType::GetTypeDocs {
1173            language: "rust".to_string(),
1174            workspace_root: "/example/project".to_string(),
1175            type_path: "src/config.rs::Config".to_string(),
1176        },
1177    }));
1178
1179    state.send_event_replay(ChatEvent::ToolExecutionCompleted {
1180        tool_call_id: "test_get_type_docs".to_string(),
1181        tool_name: "get_type_docs".to_string(),
1182        tool_result: ToolExecutionResult::GetTypeDocs {
1183            documentation: "/// Configuration struct for the application\npub struct Config {\n    pub host: String,\n    pub port: u16,\n}".to_string(),
1184        },
1185        success: true,
1186        error: None,
1187    });
1188
1189    // Add one more retry to ensure it appears at the bottom after all the tool messages
1190    state.send_event_replay(ChatEvent::RetryAttempt {
1191        attempt: 3,
1192        max_retries: 3,
1193        backoff_ms: 8000,
1194        error: "Final retry test - should appear at the very bottom of chat".to_string(),
1195    });
1196
1197    // Simulate spawning a coordinator agent
1198    state.event_sender.send_message(ChatMessage::system(
1199        "🔄 Spawning agent for task: Coordinate multiple sub-tasks for testing".to_string(),
1200    ));
1201
1202    // Coordinator agent sends a message and uses a tool
1203    state.event_sender.send_message(ChatMessage::assistant(
1204        "coordinator".to_string(),
1205        "I'll coordinate this workflow by spawning a review agent.".to_string(),
1206        vec![ToolUseData {
1207            id: "test_coord_tool".to_string(),
1208            name: "function".to_string(),
1209            arguments: json!({
1210                "name": "set_tracked_files",
1211                "arguments": {
1212                    "file_paths": ["/example/test.rs"]
1213                }
1214            }),
1215        }],
1216        ModelInfo {
1217            model: crate::ai::model::Model::GrokCodeFast1,
1218        },
1219        TokenUsage {
1220            input_tokens: 50,
1221            output_tokens: 25,
1222            total_tokens: 75,
1223            cached_prompt_tokens: None,
1224            reasoning_tokens: None,
1225            cache_creation_input_tokens: None,
1226        },
1227        None,
1228    ));
1229
1230    // Simulate the coordinator spawning a review agent
1231    state.event_sender.send_message(ChatMessage::system(
1232        "🔄 Spawning agent for task: Review the code changes".to_string(),
1233    ));
1234
1235    // Review agent sends a message and uses a tool
1236    state.event_sender.send_message(ChatMessage::assistant(
1237        "review".to_string(),
1238        "Reviewing the changes now. I'll check for potential issues.".to_string(),
1239        vec![ToolUseData {
1240            id: "test_review_tool".to_string(),
1241            name: "function".to_string(),
1242            arguments: json!({
1243                "name": "set_tracked_files",
1244                "arguments": {
1245                    "file_paths": ["/example/test.rs", "/example/lib.rs"]
1246                }
1247            }),
1248        }],
1249        ModelInfo {
1250            model: crate::ai::model::Model::GrokCodeFast1,
1251        },
1252        TokenUsage {
1253            input_tokens: 100,
1254            output_tokens: 50,
1255            total_tokens: 150,
1256            cached_prompt_tokens: None,
1257            reasoning_tokens: None,
1258            cache_creation_input_tokens: None,
1259        },
1260        None,
1261    ));
1262
1263    // Simulate complete_task being called
1264    state.send_event_replay(ChatEvent::ToolRequest(ToolRequest {
1265        tool_call_id: "test_complete_task".to_string(),
1266        tool_name: "complete_task".to_string(),
1267        tool_type: ToolRequestType::Other { args: json!({}) },
1268    }));
1269
1270    state.send_event_replay(ChatEvent::ToolExecutionCompleted {
1271        tool_call_id: "test_complete_task".to_string(),
1272        tool_name: "complete_task".to_string(),
1273        tool_result: ToolExecutionResult::Other {
1274            result: json!({
1275                "status": "success",
1276                "message": "Review completed successfully"
1277            }),
1278        },
1279        success: true,
1280        error: None,
1281    });
1282
1283    state.event_sender.send_message(ChatMessage::system(
1284        "✅ Sub-agent completed successfully:\nReview completed successfully".to_string(),
1285    ));
1286
1287    // Add a comprehensive markdown test message for copy button testing
1288    let markdown_test = r#"# TyCode Debug UI - Markdown Test
1289
1290This is a comprehensive test message with extensive markdown formatting to test the copy button functionality.
1291
1292## Code Examples
1293
1294Here's a simple Python function:
1295
1296```python
1297def fibonacci(n):
1298    """Calculate the nth Fibonacci number."""
1299    if n <= 1:
1300        return n
1301    return fibonacci(n-1) + fibonacci(n-2)
1302
1303# Test the function
1304for i in range(10):
1305    print(f"F({i}) = {fibonacci(i)}")
1306```
1307
1308And here's a TypeScript example:
1309
1310```typescript
1311interface User {
1312    id: string;
1313    name: string;
1314    email: string;
1315    createdAt: Date;
1316}
1317
1318class UserService {
1319    private users: Map<string, User> = new Map();
1320
1321    async createUser(name: string, email: string): Promise<User> {
1322        const user: User = {
1323            id: crypto.randomUUID(),
1324            name,
1325            email,
1326            createdAt: new Date()
1327        };
1328        this.users.set(user.id, user);
1329        return user;
1330    }
1331
1332    async getUser(id: string): Promise<User | undefined> {
1333        return this.users.get(id);
1334    }
1335}
1336```
1337
1338## Rust Code
1339
1340Here's a Rust implementation:
1341
1342```rust
1343use std::collections::HashMap;
1344
1345#[derive(Debug, Clone)]
1346pub struct Config {
1347    pub host: String,
1348    pub port: u16,
1349    pub debug: bool,
1350}
1351
1352impl Config {
1353    pub fn new(host: String, port: u16) -> Self {
1354        Self {
1355            host,
1356            port,
1357            debug: false,
1358        }
1359    }
1360
1361    pub fn with_debug(mut self, debug: bool) -> Self {
1362        self.debug = debug;
1363        self
1364    }
1365}
1366
1367fn main() {
1368    let config = Config::new("localhost".to_string(), 8080)
1369        .with_debug(true);
1370    println!("Config: {:?}", config);
1371}
1372```
1373
1374## Lists and Text Formatting
1375
1376### Unordered List
1377
1378- **Bold text** for emphasis
1379- *Italic text* for subtle emphasis
1380- `Inline code` for variable names
1381- [Links to documentation](https://example.com)
1382
1383### Ordered List
1384
13851. First step: Initialize the project
13862. Second step: Install dependencies
13873. Third step: Configure settings
13884. Fourth step: Run tests
13895. Fifth step: Deploy to production
1390
1391### Nested Lists
1392
1393- Top level item
1394  - Nested item 1
1395  - Nested item 2
1396    - Double nested item
1397- Another top level item
1398  - More nesting
1399
1400## Blockquotes
1401
1402> This is a blockquote with important information.
1403> It can span multiple lines and provides context
1404> for the discussion at hand.
1405
1406> **Note:** Always test your code before deploying to production!
1407
1408## Tables
1409
1410| Feature | Status | Priority |
1411|---------|--------|----------|
1412| Copy button | ✅ Done | High |
1413| Insert button | ❌ Removed | N/A |
1414| Message copy | ✅ Done | High |
1415| Line numbers | ✅ Fixed | Medium |
1416
1417## More Code Examples
1418
1419Bash script:
1420
1421```bash
1422#!/bin/bash
1423
1424for file in *.tar.xz; do
1425  if [ -f "$file" ]; then
1426    echo "Verifying attestation for $file"
1427    gh attestation verify "$file" --owner tigy32
1428  fi
1429done
1430```
1431
1432SQL query:
1433
1434```sql
1435SELECT u.id, u.name, COUNT(o.id) as order_count
1436FROM users u
1437LEFT JOIN orders o ON u.id = o.user_id
1438WHERE u.created_at > '2024-01-01'
1439GROUP BY u.id, u.name
1440HAVING COUNT(o.id) > 5
1441ORDER BY order_count DESC;
1442```
1443
1444JSON configuration:
1445
1446```json
1447{
1448  "name": "tycode-vscode",
1449  "version": "1.0.0",
1450  "dependencies": {
1451    "vscode": "^1.80.0"
1452  },
1453  "scripts": {
1454    "compile": "webpack",
1455    "test": "node ./out/test/runTest.js"
1456  }
1457}
1458```
1459
1460## Conclusion
1461
1462This debug message contains:
1463- Multiple code blocks with syntax highlighting
1464- Headings at various levels
1465- Lists (ordered, unordered, nested)
1466- Text formatting (bold, italic, inline code)
1467- Blockquotes
1468- Tables
1469- Links
1470
1471**Test the copy button** by clicking the ⧉ button at the bottom of this message!"#;
1472
1473    state.event_sender.send_message(ChatMessage::assistant(
1474        "debug".to_string(),
1475        markdown_test.to_string(),
1476        vec![],
1477        ModelInfo {
1478            model: crate::ai::model::Model::GrokCodeFast1,
1479        },
1480        TokenUsage {
1481            input_tokens: 500,
1482            output_tokens: 1000,
1483            total_tokens: 1500,
1484            cached_prompt_tokens: None,
1485            reasoning_tokens: None,
1486            cache_creation_input_tokens: None,
1487        },
1488        None,
1489    ));
1490
1491    vec![create_message(
1492        "Debug UI test completed. Check:\n1. Retry counter messages should always be at the bottom of chat\n2. View Diff button should be visible even with very long file paths (text should truncate with ...)\n3. Agent spawning messages and complete_task should appear correctly\n4. Long markdown message with copy button for testing copy functionality".to_string(),
1493        MessageSender::System,
1494    )]
1495}
1496
1497async fn handle_profile_command(state: &mut ActorState, parts: &[&str]) -> Vec<ChatMessage> {
1498    let show_current = parts.len() < 2 || parts[1].to_lowercase() == "show";
1499    if show_current {
1500        let current = state.settings.current_profile().unwrap_or("default");
1501        return vec![create_message(
1502            format!("Current profile: {}", current),
1503            MessageSender::System,
1504        )];
1505    }
1506
1507    let subcommand = parts[1].to_lowercase();
1508    match subcommand.as_str() {
1509        "list" => {
1510            let home = match dirs::home_dir() {
1511                Some(h) => h,
1512                None => {
1513                    return vec![create_message(
1514                        "Failed to get home directory.".to_string(),
1515                        MessageSender::Error,
1516                    )];
1517                }
1518            };
1519
1520            let tycode_dir = home.join(".tycode");
1521            let mut profiles: Vec<String> = vec!["default".to_string()];
1522
1523            if tycode_dir.exists() {
1524                match fs::read_dir(&tycode_dir) {
1525                    Ok(entries) => {
1526                        for entry in entries {
1527                            match entry {
1528                                Ok(e) => {
1529                                    let path = e.path();
1530                                    let file_name = path.file_name().and_then(|n| n.to_str());
1531                                    if let Some(name) = file_name {
1532                                        if let Some(profile_name) = name
1533                                            .strip_prefix("settings_")
1534                                            .and_then(|s| s.strip_suffix(".toml"))
1535                                        {
1536                                            if !profile_name.is_empty() {
1537                                                profiles.push(profile_name.to_string());
1538                                            }
1539                                        }
1540                                    }
1541                                }
1542                                Err(_) => {
1543                                    // ignore
1544                                }
1545                            }
1546                        }
1547                    }
1548                    Err(_) => {
1549                        return vec![create_message(
1550                            "Failed to read .tycode directory.".to_string(),
1551                            MessageSender::Error,
1552                        )];
1553                    }
1554                }
1555            }
1556
1557            profiles.sort();
1558            let msg = format!("Available profiles: {}", profiles.join(", "));
1559            vec![create_message(msg, MessageSender::System)]
1560        }
1561        "switch" => {
1562            if parts.len() < 3 {
1563                return vec![create_message(
1564                    "Usage: /profile switch <name>".to_string(),
1565                    MessageSender::Error,
1566                )];
1567            }
1568            let name = parts[2];
1569            if let Err(e) = state.settings.switch_profile(name) {
1570                return vec![create_message(
1571                    format!("Failed to switch to {}: {}", name, e),
1572                    MessageSender::Error,
1573                )];
1574            }
1575            if let Err(e) = state.settings.save() {
1576                return vec![create_message(
1577                    format!("Switched to profile {}, but failed to persist: {}", name, e),
1578                    MessageSender::Error,
1579                )];
1580            }
1581            match state.reload_from_settings().await {
1582                Ok(()) => vec![create_message(
1583                    format!("Switched to profile: {}.", name),
1584                    MessageSender::System,
1585                )],
1586                Err(e) => vec![create_message(
1587                    format!(
1588                        "Switched to profile: {}, but failed to reload: {}",
1589                        name,
1590                        e.to_string()
1591                    ),
1592                    MessageSender::Error,
1593                )],
1594            }
1595        }
1596        "save" => {
1597            if parts.len() < 3 {
1598                return vec![create_message(
1599                    "Usage: /profile save <name>".to_string(),
1600                    MessageSender::Error,
1601                )];
1602            }
1603            let name = parts[2];
1604            if let Err(e) = state.settings.save_as_profile(name) {
1605                return vec![create_message(
1606                    format!("Failed to save as {}: {}", name, e),
1607                    MessageSender::Error,
1608                )];
1609            }
1610            vec![create_message(
1611                format!("Saved current settings as profile: {}.", name),
1612                MessageSender::System,
1613            )]
1614        }
1615        _ => vec![create_message(
1616            format!(
1617                "Unknown subcommand '{}'. Usage: /profile [switch|save|list|show] [<name>]",
1618                subcommand
1619            ),
1620            MessageSender::Error,
1621        )],
1622    }
1623}
1624
1625async fn handle_sessions_command(state: &mut ActorState, parts: &[&str]) -> Vec<ChatMessage> {
1626    if parts.len() < 2 {
1627        return vec![create_message(
1628            "Usage: /sessions [list|resume <id>|delete <id>|gc [days]]".to_string(),
1629            MessageSender::System,
1630        )];
1631    }
1632
1633    match parts[1] {
1634        "list" => handle_sessions_list_command(state).await,
1635        "resume" => handle_sessions_resume_command(state, parts).await,
1636        "delete" => handle_sessions_delete_command(state, parts).await,
1637        "gc" => handle_sessions_gc_command(state, parts).await,
1638        _ => vec![create_message(
1639            format!(
1640                "Unknown sessions subcommand: {}. Use: list, resume, delete, gc",
1641                parts[1]
1642            ),
1643            MessageSender::Error,
1644        )],
1645    }
1646}
1647
1648async fn handle_sessions_list_command(state: &ActorState) -> Vec<ChatMessage> {
1649    let sessions = match storage::list_sessions(Some(&state.sessions_dir)) {
1650        Ok(s) => s,
1651        Err(e) => {
1652            return vec![create_message(
1653                format!("Failed to list sessions: {e:?}"),
1654                MessageSender::Error,
1655            )];
1656        }
1657    };
1658
1659    if sessions.is_empty() {
1660        return vec![create_message(
1661            "No saved sessions found.".to_string(),
1662            MessageSender::System,
1663        )];
1664    }
1665
1666    let mut message = String::from("=== Saved Sessions ===\n\n");
1667    for session_meta in sessions {
1668        let created = chrono::DateTime::from_timestamp_millis(session_meta.created_at as i64)
1669            .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
1670            .unwrap_or_else(|| session_meta.created_at.to_string());
1671
1672        let modified = chrono::DateTime::from_timestamp_millis(session_meta.last_modified as i64)
1673            .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
1674            .unwrap_or_else(|| session_meta.last_modified.to_string());
1675
1676        message.push_str(&format!(
1677            "  ID: {}\n    Task List: {}\n    Preview: {}\n    Created: {}\n    Last modified: {}\n\n",
1678            session_meta.id, session_meta.task_list_title, session_meta.preview, created, modified
1679        ));
1680    }
1681
1682    message.push_str("Use `/sessions resume <id>` to load a session.\n");
1683
1684    vec![create_message(message, MessageSender::System)]
1685}
1686
1687async fn handle_sessions_resume_command(
1688    state: &mut ActorState,
1689    parts: &[&str],
1690) -> Vec<ChatMessage> {
1691    if parts.len() < 3 {
1692        return vec![create_message(
1693            "Usage: /sessions resume <id>".to_string(),
1694            MessageSender::Error,
1695        )];
1696    }
1697
1698    let session_id = parts[2];
1699
1700    match resume_session(state, session_id).await {
1701        Ok(()) => vec![create_message(
1702            format!("Session '{}' resumed successfully.", session_id),
1703            MessageSender::System,
1704        )],
1705        Err(e) => vec![create_message(
1706            format!("Failed to resume session '{}': {e:?}", session_id),
1707            MessageSender::Error,
1708        )],
1709    }
1710}
1711
1712async fn handle_sessions_delete_command(state: &ActorState, parts: &[&str]) -> Vec<ChatMessage> {
1713    if parts.len() < 3 {
1714        return vec![create_message(
1715            "Usage: /sessions delete <id>".to_string(),
1716            MessageSender::Error,
1717        )];
1718    }
1719
1720    let session_id = parts[2];
1721
1722    match storage::delete_session(session_id, Some(&state.sessions_dir)) {
1723        Ok(()) => vec![create_message(
1724            format!("Session '{}' deleted successfully.", session_id),
1725            MessageSender::System,
1726        )],
1727        Err(e) => vec![create_message(
1728            format!("Failed to delete session '{}': {e:?}", session_id),
1729            MessageSender::Error,
1730        )],
1731    }
1732}
1733
1734async fn handle_sessions_gc_command(state: &ActorState, parts: &[&str]) -> Vec<ChatMessage> {
1735    let days = if parts.len() >= 3 {
1736        match parts[2].parse::<u64>() {
1737            Ok(d) => d,
1738            Err(_) => {
1739                return vec![create_message(
1740                    "Usage: /sessions gc [days]. Days must be a positive number.".to_string(),
1741                    MessageSender::Error,
1742                )];
1743            }
1744        }
1745    } else {
1746        7
1747    };
1748
1749    let sessions = match storage::list_sessions(Some(&state.sessions_dir)) {
1750        Ok(s) => s,
1751        Err(e) => {
1752            return vec![create_message(
1753                format!("Failed to list sessions: {e:?}"),
1754                MessageSender::Error,
1755            )];
1756        }
1757    };
1758
1759    let cutoff_time = Utc::now().timestamp_millis() as u64 - (days * 24 * 60 * 60 * 1000);
1760    let mut deleted_count = 0;
1761    let mut failed_deletes = Vec::new();
1762
1763    for session_meta in sessions {
1764        if session_meta.last_modified >= cutoff_time {
1765            continue;
1766        }
1767
1768        match storage::delete_session(&session_meta.id, Some(&state.sessions_dir)) {
1769            Ok(()) => deleted_count += 1,
1770            Err(e) => {
1771                failed_deletes.push(format!("{}: {e:?}", session_meta.id));
1772            }
1773        }
1774    }
1775
1776    let mut message = format!(
1777        "Garbage collection complete. Deleted {} session(s) older than {} days.",
1778        deleted_count, days
1779    );
1780
1781    if !failed_deletes.is_empty() {
1782        message.push_str(&format!(
1783            "\n\nFailed to delete {} session(s):\n",
1784            failed_deletes.len()
1785        ));
1786        for failure in failed_deletes {
1787            message.push_str(&format!("  {failure}\n"));
1788        }
1789    }
1790
1791    vec![create_message(message, MessageSender::System)]
1792}