返回 CodeWhale
task.rs
根目录 / crates / tui / src / commands / groups / utility / task.rs
1 //! Task commands: add/list/show/cancel
2
3 use codewhale_command_contract::handler::{CommandCapabilities, CommandContexts, CommandHandler};
4 use codewhale_command_contract::metadata::{CommandInfo, RegisterCommand};
5
6 use crate::commands::CommandResult;
7 use crate::tui::app::AppAction;
8
9 pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
10 name: "task",
11 aliases: &["tasks"],
12 usage: "/task [add <prompt>|list|digest|show <id>|cancel <id>]",
13 description_key: "cmd_task_description",
14 };
15
16 pub(in crate::commands) struct TaskCmd;
17
18 impl RegisterCommand<CommandResult> for TaskCmd {
19 fn info() -> &'static CommandInfo {
20 &COMMAND_INFO
21 }
22
23 fn handler() -> CommandHandler<CommandResult> {
24 CommandHandler::Contextual {
25 capabilities: CommandCapabilities::WORKSPACE,
26 handler: task_contextual,
27 }
28 }
29 }
30
31 fn task_contextual(contexts: CommandContexts<'_>, arg: Option<&str>) -> CommandResult {
32 let mut parts = contexts.into_parts();
33 let Some(workspace) = parts.workspace.as_deref_mut() else {
34 return CommandResult::error("Command capability unavailable: workspace");
35 };
36 task(workspace, arg)
37 }
38
39 fn task(
40 workspace: &mut dyn codewhale_command_contract::facets::CommandWorkspaceContext,
41 args: Option<&str>,
42 ) -> CommandResult {
43 let raw = args.unwrap_or("").trim();
44 if raw.is_empty() || raw.eq_ignore_ascii_case("list") {
45 return CommandResult::action(AppAction::TaskList);
46 }
47
48 let mut parts = raw.splitn(2, char::is_whitespace);
49 let action = parts.next().unwrap_or("").to_ascii_lowercase();
50 let remainder = parts.next().map(str::trim).filter(|s| !s.is_empty());
51
52 match action.as_str() {
53 "add" => {
54 let Some(prompt) = remainder else {
55 return CommandResult::error("Usage: /task add <prompt>");
56 };
57 CommandResult::action(AppAction::TaskAdd {
58 prompt: prompt.to_string(),
59 })
60 }
61 "list" => CommandResult::action(AppAction::TaskList),
62 "digest" => match workspace.operation_digest() {
63 Ok(text) => CommandResult::message(text),
64 Err(error) => CommandResult::error(error),
65 },
66 "show" => {
67 let Some(id) = remainder else {
68 return CommandResult::error("Usage: /task show <id>");
69 };
70 CommandResult::action(AppAction::TaskShow { id: id.to_string() })
71 }
72 "cancel" | "stop" => {
73 let Some(id) = remainder else {
74 return CommandResult::error("Usage: /task cancel <id>");
75 };
76 CommandResult::action(AppAction::TaskCancel { id: id.to_string() })
77 }
78 _ => CommandResult::error("Usage: /task [add <prompt>|list|digest|show <id>|cancel <id>]"),
79 }
80 }
81
82 #[cfg(test)]
83 mod tests {
84 use super::*;
85 use std::path::PathBuf;
86
87 struct FakeWorkspace;
88 impl codewhale_command_contract::facets::CommandWorkspaceContext for FakeWorkspace {
89 fn workspace(&self) -> PathBuf {
90 PathBuf::from(".")
91 }
92 fn work_state_snapshot(&self) -> Result<Option<String>, String> {
93 Ok(None)
94 }
95 fn operation_digest(&mut self) -> Result<String, String> {
96 Ok("No active operations or to-do items.".to_string())
97 }
98 }
99
100 struct FailingWorkspace;
101 impl codewhale_command_contract::facets::CommandWorkspaceContext for FailingWorkspace {
102 fn workspace(&self) -> PathBuf {
103 PathBuf::from(".")
104 }
105 fn work_state_snapshot(&self) -> Result<Option<String>, String> {
106 Ok(None)
107 }
108 fn operation_digest(&mut self) -> Result<String, String> {
109 Err("Operation digest is temporarily unavailable: boom".to_string())
110 }
111 }
112
113 #[test]
114 fn parses_add_and_cancel() {
115 let add = task(&mut FakeWorkspace, Some("add write tests"));
116 assert!(matches!(
117 add.action,
118 Some(AppAction::TaskAdd { prompt }) if prompt == "write tests"
119 ));
120
121 let cancel = task(&mut FakeWorkspace, Some("cancel task_1234"));
122 assert!(matches!(
123 cancel.action,
124 Some(AppAction::TaskCancel { id }) if id == "task_1234"
125 ));
126 }
127
128 #[test]
129 fn validates_usage() {
130 let result = task(&mut FakeWorkspace, Some("add"));
131 assert!(result.message.is_some());
132 assert!(result.action.is_none());
133 }
134
135 #[test]
136 fn digest_uses_canonical_work_runtime_without_another_state_store() {
137 let result = task(&mut FakeWorkspace, Some("digest"));
138 assert_eq!(
139 result.message.as_deref(),
140 Some("No active operations or to-do items.")
141 );
142 assert!(result.action.is_none());
143
144 let failing = task(&mut FailingWorkspace, Some("digest"));
145 assert!(failing.is_error);
146 assert!(
147 failing
148 .message
149 .as_deref()
150 .unwrap_or_default()
151 .contains("Operation digest is temporarily unavailable: boom")
152 );
153 }
154
155 #[test]
156 fn handler_is_contextual() {
157 let CommandHandler::Contextual {
158 capabilities,
159 handler,
160 } = TaskCmd::handler()
161 else {
162 panic!("task must be contextual");
163 };
164 assert_eq!(capabilities, CommandCapabilities::WORKSPACE);
165 let missing = handler(CommandContexts::empty(), Some("list"));
166 assert!(missing.is_error);
167 assert_eq!(
168 missing.message.as_deref(),
169 Some("Error: Command capability unavailable: workspace")
170 );
171 assert_eq!(TaskCmd::info().description_key, "cmd_task_description");
172 assert_eq!(TaskCmd::info().aliases, &["tasks"]);
173 }
174 }
175
175 lines RUST