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
85pub 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
130pub 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
151fn 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
255pub 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 return vec![create_message(
360 "Usage: /cost set <free|low|medium|high|unlimited>".to_string(),
361 MessageSender::Error,
362 )];
363 }
364
365 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 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 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 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 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 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 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 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 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 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 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 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 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 state.event_sender.send_message(ChatMessage::system(
966 "🔄 Spawning agent for task: Testing UI bug fixes".to_string(),
967 ));
968
969 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 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 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 for tool_request in &tool_requests {
1063 state.send_event_replay(ChatEvent::ToolRequest(tool_request.clone()));
1064 }
1065
1066 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 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 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 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 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 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 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 state.event_sender.send_message(ChatMessage::system(
1199 "🔄 Spawning agent for task: Coordinate multiple sub-tasks for testing".to_string(),
1200 ));
1201
1202 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 state.event_sender.send_message(ChatMessage::system(
1232 "🔄 Spawning agent for task: Review the code changes".to_string(),
1233 ));
1234
1235 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 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 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 }
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}