返回 CodeWhale
review.rs
根目录 / crates / tui / src / commands / groups / skills / review.rs
1 //! Review command: activate review skill and send a target immediately.
2
3 use crate::skills::{SkillRegistry, default_skills_dir};
4 use crate::tui::app::{App, AppAction};
5 use crate::tui::history::HistoryCell;
6
7 use crate::commands::CommandResult;
8
9 fn warnings_suffix(registry: &SkillRegistry) -> String {
10 if registry.warnings().is_empty() {
11 return String::new();
12 }
13
14 format!("\n\nWarnings:\n- {}", registry.warnings().join("\n- "))
15 }
16
17 fn review(app: &mut App, args: Option<&str>) -> CommandResult {
18 let target = args.unwrap_or("").trim();
19 if target.is_empty() {
20 return CommandResult::error("Usage: /review <target>");
21 }
22
23 let skills_dir = app.skills_dir.clone();
24 let registry = SkillRegistry::discover(&skills_dir).into_enabled();
25 let mut warnings = warnings_suffix(&registry);
26 let mut skill = registry.get("review").cloned();
27
28 let global_dir = default_skills_dir();
29 if skill.is_none() && global_dir != skills_dir {
30 let registry = SkillRegistry::discover(&global_dir).into_enabled();
31 if warnings.is_empty() {
32 warnings = warnings_suffix(&registry);
33 } else if !registry.warnings().is_empty() {
34 warnings.push_str(&format!("\n- {}", registry.warnings().join("\n- ")));
35 }
36 skill = registry.get("review").cloned();
37 }
38
39 let skill = match skill {
40 Some(skill) => skill,
41 None => {
42 let global_display = global_dir.display();
43 return CommandResult::error(format!(
44 "Review skill not found in {} or {}. Create ~/.codewhale/skills/review/SKILL.md.{}",
45 skills_dir.display(),
46 global_display,
47 warnings
48 ));
49 }
50 };
51
52 let instruction = format!(
53 "You are now using a skill. Follow these instructions:\n\n# Skill: {}\n\n{}\n\n---\n\nNow respond to the user's request following the above skill instructions.",
54 skill.name, skill.body
55 );
56
57 app.add_message(HistoryCell::System {
58 content: format!("Activated skill: {}\n\n{}", skill.name, skill.description),
59 });
60 app.active_skill = Some(instruction);
61 app.active_skill_provenance = None;
62
63 CommandResult::action(AppAction::SendMessage(target.to_string()))
64 }
65
66 pub(in crate::commands) const COMMAND_INFO: crate::commands::traits::CommandInfo =
67 crate::commands::traits::CommandInfo {
68 name: "review",
69 aliases: &["shencha"],
70 usage: "/review <target>",
71 description_id: crate::localization::MessageId::CmdReviewDescription,
72 };
73
74 pub(in crate::commands) struct ReviewCmd;
75
76 impl crate::commands::traits::RegisterCommand for ReviewCmd {
77 fn info() -> &'static crate::commands::traits::CommandInfo {
78 &COMMAND_INFO
79 }
80
81 fn execute(
82 app: &mut crate::tui::app::App,
83 arg: Option<&str>,
84 ) -> crate::commands::CommandResult {
85 review(app, arg)
86 }
87 }
88
89 #[cfg(test)]
90 mod tests {
91 use super::*;
92 use crate::config::Config;
93 use crate::tui::app::{App, TuiOptions};
94 use tempfile::TempDir;
95
96 fn create_test_app_with_tmpdir(tmpdir: &TempDir) -> App {
97 let options = TuiOptions {
98 skills_dir: tmpdir.path().join("skills"),
99 memory_path: tmpdir.path().join("memory.md"),
100 notes_path: tmpdir.path().join("notes.txt"),
101 mcp_config_path: tmpdir.path().join("mcp.json"),
102 ..crate::test_support::test_tui_options(tmpdir.path())
103 };
104 App::new(options, &Config::default())
105 }
106
107 fn create_review_skill_dir(tmpdir: &TempDir) {
108 let skill_dir = tmpdir.path().join("skills").join("review");
109 std::fs::create_dir_all(&skill_dir).unwrap();
110 std::fs::write(
111 skill_dir.join("SKILL.md"),
112 "---\nname: review\ndescription: Code review skill\n---\nReview the code",
113 )
114 .unwrap();
115 }
116
117 #[test]
118 fn test_review_without_target() {
119 let tmpdir = TempDir::new().unwrap();
120 let mut app = create_test_app_with_tmpdir(&tmpdir);
121 let result = review(&mut app, None);
122 assert!(result.message.is_some());
123 assert!(result.message.unwrap().contains("Usage: /review"));
124 }
125
126 #[test]
127 fn test_review_without_skill_installed() {
128 let tmpdir = TempDir::new().unwrap();
129 let mut app = create_test_app_with_tmpdir(&tmpdir);
130 // Set skills dir to empty temp dir
131 app.skills_dir = tmpdir.path().join("nonexistent_skills");
132 let result = review(&mut app, Some("file.rs"));
133 // The command should either error about missing skill or work if global skill exists
134 assert!(result.message.is_some() || result.action.is_some());
135 }
136
137 #[test]
138 fn test_review_with_skill_activates_and_sends() {
139 let tmpdir = TempDir::new().unwrap();
140 create_review_skill_dir(&tmpdir);
141 let mut app = create_test_app_with_tmpdir(&tmpdir);
142 let result = review(&mut app, Some("file.rs"));
143 assert!(result.message.is_none());
144 assert!(matches!(result.action, Some(AppAction::SendMessage(_))));
145 assert!(app.active_skill.is_some());
146 assert!(!app.history.is_empty());
147 }
148 }
149
149 lines RUST