@@ -764,6 +764,8 @@ enum PermissionMode {
#[cfg(test)]
pub(crate) mod tests {
use super::*;
+ use crate::e2e_tests;
+ use gpui::TestAppContext;
use serde_json::json;
crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow");
@@ -776,6 +778,68 @@ pub(crate) mod tests {
}
}
+ #[gpui::test]
+ #[cfg_attr(not(feature = "e2e"), ignore)]
+ async fn test_todo_plan(cx: &mut TestAppContext) {
+ let fs = e2e_tests::init_test(cx).await;
+ let project = Project::test(fs, [], cx).await;
+ let thread =
+ e2e_tests::new_test_thread(ClaudeCode, project.clone(), "/private/tmp", cx).await;
+
+ thread
+ .update(cx, |thread, cx| {
+ thread.send_raw(
+ "Create a todo plan for initializing a new React app. I'll follow it myself, do not execute on it.",
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ let mut entries_len = 0;
+
+ thread.read_with(cx, |thread, _| {
+ entries_len = thread.plan().entries.len();
+ assert!(thread.plan().entries.len() > 0, "Empty plan");
+ });
+
+ thread
+ .update(cx, |thread, cx| {
+ thread.send_raw(
+ "Mark the first entry status as in progress without acting on it.",
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ thread.read_with(cx, |thread, _| {
+ assert!(matches!(
+ thread.plan().entries[0].status,
+ acp::PlanEntryStatus::InProgress
+ ));
+ assert_eq!(thread.plan().entries.len(), entries_len);
+ });
+
+ thread
+ .update(cx, |thread, cx| {
+ thread.send_raw(
+ "Now mark the first entry as completed without acting on it.",
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ thread.read_with(cx, |thread, _| {
+ assert!(matches!(
+ thread.plan().entries[0].status,
+ acp::PlanEntryStatus::Completed
+ ));
+ assert_eq!(thread.plan().entries.len(), entries_len);
+ });
+ }
+
#[test]
fn test_deserialize_content_untagged_text() {
let json = json!("Hello, world!");
@@ -143,25 +143,6 @@ impl ClaudeTool {
Self::Grep(Some(params)) => vec![format!("`{params}`").into()],
Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()],
Self::WebSearch(Some(params)) => vec![params.to_string().into()],
- Self::TodoWrite(Some(params)) => vec![
- params
- .todos
- .iter()
- .map(|todo| {
- format!(
- "- {} {}: {}",
- match todo.status {
- TodoStatus::Completed => "✅",
- TodoStatus::InProgress => "🚧",
- TodoStatus::Pending => "⬜",
- },
- todo.priority,
- todo.content
- )
- })
- .join("\n")
- .into(),
- ],
Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()],
Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
@@ -193,6 +174,10 @@ impl ClaudeTool {
})
.unwrap_or_default()
}
+ Self::TodoWrite(Some(_)) => {
+ // These are mapped to plan updates later
+ vec![]
+ }
Self::Task(None)
| Self::NotebookRead(None)
| Self::NotebookEdit(None)
@@ -488,10 +473,11 @@ impl std::fmt::Display for GrepToolParams {
}
}
-#[derive(Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
+#[derive(Default, Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
#[serde(rename_all = "snake_case")]
pub enum TodoPriority {
High,
+ #[default]
Medium,
Low,
}
@@ -526,14 +512,13 @@ impl Into<acp::PlanEntryStatus> for TodoStatus {
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
pub struct Todo {
- /// Unique identifier
- pub id: String,
/// Task description
pub content: String,
- /// Priority level of the todo
- pub priority: TodoPriority,
/// Current status of the todo
pub status: TodoStatus,
+ /// Priority level of the todo
+ #[serde(default)]
+ pub priority: TodoPriority,
}
impl Into<acp::PlanEntry> for Todo {