feat: pin todo list (#464)
* wip: backend todo normalisation * fe implementation * remove unused dep * cursor return ActionType::TodoManagement * use lucide icons rather than emojis in the todo list * review comments
This commit is contained in:
committed by
GitHub
parent
e9882b23b9
commit
ba8650cfca
@@ -13,7 +13,7 @@ use crate::{
|
||||
command::CommandBuilder,
|
||||
executors::{ExecutorError, StandardCodingAgentExecutor},
|
||||
logs::{
|
||||
ActionType, EditDiff, NormalizedEntry, NormalizedEntryType,
|
||||
ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, TodoItem as LogsTodoItem,
|
||||
stderr_processor::normalize_stderr_logs,
|
||||
utils::{EntryIndexProvider, patch::ConversationPatch},
|
||||
},
|
||||
@@ -499,8 +499,21 @@ impl AmpContentItem {
|
||||
AmpToolData::List { .. } => ActionType::Other {
|
||||
description: "List directory".to_string(),
|
||||
},
|
||||
AmpToolData::Todo { .. } => ActionType::Other {
|
||||
description: "Manage TODO list".to_string(),
|
||||
AmpToolData::Todo { todos } => ActionType::TodoManagement {
|
||||
todos: todos
|
||||
.as_ref()
|
||||
.map(|todos| {
|
||||
todos
|
||||
.iter()
|
||||
.map(|t| LogsTodoItem {
|
||||
content: t.content.clone(),
|
||||
status: t.status.clone(),
|
||||
priority: t.priority.clone(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
operation: "write".to_string(),
|
||||
},
|
||||
AmpToolData::Unknown { .. } => ActionType::Other {
|
||||
description: format!("Tool: {tool_name}"),
|
||||
@@ -522,32 +535,10 @@ impl AmpContentItem {
|
||||
ActionType::WebFetch { url } => format!("`{url}`"),
|
||||
ActionType::PlanPresentation { plan } => format!("Plan Presentation: `{plan}`"),
|
||||
ActionType::TaskCreate { description } => description.clone(),
|
||||
ActionType::TodoManagement { .. } => "TODO list updated".to_string(),
|
||||
ActionType::Other { description: _ } => {
|
||||
// For other tools, try to extract key information or fall back to tool name
|
||||
match input {
|
||||
AmpToolData::Todo { todos, .. } => {
|
||||
if let Some(todos) = todos {
|
||||
let mut todo_items = Vec::new();
|
||||
for todo in todos {
|
||||
let emoji = match todo.status.as_str() {
|
||||
"completed" => "✅",
|
||||
"in_progress" | "in-progress" => "🔄",
|
||||
"pending" | "todo" => "⏳",
|
||||
_ => "📝",
|
||||
};
|
||||
let priority = todo.priority.as_deref().unwrap_or("medium");
|
||||
todo_items
|
||||
.push(format!("{} {} ({})", emoji, todo.content, priority));
|
||||
}
|
||||
if !todo_items.is_empty() {
|
||||
format!("TODO List:\n{}", todo_items.join("\n"))
|
||||
} else {
|
||||
"Managing TODO list".to_string()
|
||||
}
|
||||
} else {
|
||||
"Managing TODO list".to_string()
|
||||
}
|
||||
}
|
||||
AmpToolData::List { path, .. } => {
|
||||
if let Some(path) = path {
|
||||
let relative_path = make_path_relative(path, worktree_path);
|
||||
@@ -584,6 +575,19 @@ impl AmpContentItem {
|
||||
}
|
||||
parts.join(" ")
|
||||
}
|
||||
AmpToolData::Unknown { data } => {
|
||||
// Manually check if "name" is prefixed with "todo"
|
||||
// This is a hack to avoid flickering on the frontend
|
||||
let name = data
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(tool_name);
|
||||
if name.starts_with("todo") {
|
||||
"TODO list updated".to_string()
|
||||
} else {
|
||||
tool_name.to_string()
|
||||
}
|
||||
}
|
||||
_ => tool_name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
command::CommandBuilder,
|
||||
executors::{ExecutorError, StandardCodingAgentExecutor},
|
||||
logs::{
|
||||
ActionType, EditDiff, NormalizedEntry, NormalizedEntryType,
|
||||
ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, TodoItem,
|
||||
stderr_processor::normalize_stderr_logs,
|
||||
utils::{EntryIndexProvider, patch::ConversationPatch},
|
||||
},
|
||||
@@ -505,8 +505,16 @@ impl ClaudeLogProcessor {
|
||||
path: make_path_relative(notebook_path, worktree_path),
|
||||
diffs: vec![],
|
||||
},
|
||||
ClaudeToolData::TodoWrite { .. } => ActionType::Other {
|
||||
description: "Manage TODO list".to_string(),
|
||||
ClaudeToolData::TodoWrite { todos } => ActionType::TodoManagement {
|
||||
todos: todos
|
||||
.iter()
|
||||
.map(|t| TodoItem {
|
||||
content: t.content.clone(),
|
||||
status: t.status.clone(),
|
||||
priority: t.priority.clone(),
|
||||
})
|
||||
.collect(),
|
||||
operation: "write".to_string(),
|
||||
},
|
||||
ClaudeToolData::Glob { pattern, path: _ } => ActionType::Search {
|
||||
query: pattern.clone(),
|
||||
@@ -534,26 +542,8 @@ impl ClaudeLogProcessor {
|
||||
ActionType::WebFetch { url } => format!("`{url}`"),
|
||||
ActionType::TaskCreate { description } => description.clone(),
|
||||
ActionType::PlanPresentation { plan } => plan.clone(),
|
||||
ActionType::TodoManagement { .. } => "TODO list updated".to_string(),
|
||||
ActionType::Other { description: _ } => match tool_data {
|
||||
ClaudeToolData::TodoWrite { todos } => {
|
||||
let mut todo_items = Vec::new();
|
||||
for todo in todos {
|
||||
let status_emoji = match todo.status.as_str() {
|
||||
"completed" => "✅",
|
||||
"in_progress" => "🔄",
|
||||
"pending" | "todo" => "⏳",
|
||||
_ => "📝",
|
||||
};
|
||||
let priority = todo.priority.as_deref().unwrap_or("medium");
|
||||
todo_items
|
||||
.push(format!("{} {} ({})", status_emoji, todo.content, priority));
|
||||
}
|
||||
if !todo_items.is_empty() {
|
||||
format!("TODO List:\n{}", todo_items.join("\n"))
|
||||
} else {
|
||||
"Managing TODO list".to_string()
|
||||
}
|
||||
}
|
||||
ClaudeToolData::LS { path } => {
|
||||
let relative_path = make_path_relative(path, worktree_path);
|
||||
if relative_path.is_empty() {
|
||||
@@ -843,45 +833,6 @@ mod tests {
|
||||
assert_eq!(entries[0].content, "Let me think about this...");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_tool_content_extraction() {
|
||||
// Test TodoWrite with actual todo list
|
||||
let todo_data = ClaudeToolData::TodoWrite {
|
||||
todos: vec![
|
||||
ClaudeTodoItem {
|
||||
id: Some("1".to_string()),
|
||||
content: "Fix the navigation bug".to_string(),
|
||||
status: "completed".to_string(),
|
||||
priority: Some("high".to_string()),
|
||||
},
|
||||
ClaudeTodoItem {
|
||||
id: Some("2".to_string()),
|
||||
content: "Add user authentication".to_string(),
|
||||
status: "in_progress".to_string(),
|
||||
priority: Some("medium".to_string()),
|
||||
},
|
||||
ClaudeTodoItem {
|
||||
id: Some("3".to_string()),
|
||||
content: "Write documentation".to_string(),
|
||||
status: "pending".to_string(),
|
||||
priority: Some("low".to_string()),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let action_type = ClaudeLogProcessor::extract_action_type(&todo_data, "/tmp/test-worktree");
|
||||
let result = ClaudeLogProcessor::generate_concise_content(
|
||||
&todo_data,
|
||||
&action_type,
|
||||
"/tmp/test-worktree",
|
||||
);
|
||||
|
||||
assert!(result.contains("TODO List:"));
|
||||
assert!(result.contains("✅ Fix the navigation bug (high)"));
|
||||
assert!(result.contains("🔄 Add user authentication (medium)"));
|
||||
assert!(result.contains("⏳ Write documentation (low)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_tool_empty_list() {
|
||||
// Test TodoWrite with empty todo list
|
||||
@@ -895,7 +846,7 @@ mod tests {
|
||||
"/tmp/test-worktree",
|
||||
);
|
||||
|
||||
assert_eq!(result, "Managing TODO list");
|
||||
assert_eq!(result, "TODO list updated");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
command::CommandBuilder,
|
||||
executors::{ExecutorError, StandardCodingAgentExecutor},
|
||||
logs::{
|
||||
ActionType, EditDiff, NormalizedEntry, NormalizedEntryType,
|
||||
ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, TodoItem,
|
||||
plain_text_processor::PlainTextLogProcessor,
|
||||
utils::{ConversationPatch, EntryIndexProvider},
|
||||
},
|
||||
@@ -579,16 +579,27 @@ impl CursorToolCall {
|
||||
)
|
||||
}
|
||||
CursorToolCall::Todo { args, .. } => {
|
||||
let content = if let Some(todos) = args.todos.as_ref() {
|
||||
format!("TODO List:\n{}", summarize_todos_typed(todos))
|
||||
} else {
|
||||
"Managing TODO list".to_string()
|
||||
};
|
||||
let todos = args
|
||||
.todos
|
||||
.as_ref()
|
||||
.map(|todos| {
|
||||
todos
|
||||
.iter()
|
||||
.map(|t| TodoItem {
|
||||
content: t.content.clone(),
|
||||
status: t.status.clone(),
|
||||
priority: None, // CursorTodoItem doesn't have priority field
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
(
|
||||
ActionType::Other {
|
||||
description: "Manage TODO list".to_string(),
|
||||
ActionType::TodoManagement {
|
||||
todos,
|
||||
operation: "write".to_string(),
|
||||
},
|
||||
content,
|
||||
"TODO list updated".to_string(),
|
||||
)
|
||||
}
|
||||
CursorToolCall::Unknown { .. } => (
|
||||
@@ -735,37 +746,6 @@ pub struct CursorTodoItem {
|
||||
pub dependencies: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Helpers
|
||||
=========================== */
|
||||
|
||||
fn summarize_todos_typed(items: &[CursorTodoItem]) -> String {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
for todo in items {
|
||||
let content = todo.content.as_str();
|
||||
let status = todo.status.as_str();
|
||||
let emoji = if status.eq_ignore_ascii_case("completed")
|
||||
|| status.contains("COMPLETED")
|
||||
|| status.eq_ignore_ascii_case("done")
|
||||
{
|
||||
"✅"
|
||||
} else if status.eq_ignore_ascii_case("in_progress")
|
||||
|| status.eq_ignore_ascii_case("in-progress")
|
||||
|| status.contains("IN_PROGRESS")
|
||||
{
|
||||
"🔄"
|
||||
} else {
|
||||
"⏳"
|
||||
};
|
||||
out.push(format!("{emoji} {content}"));
|
||||
}
|
||||
if out.is_empty() {
|
||||
"Managing TODO list".to_string()
|
||||
} else {
|
||||
out.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Tests
|
||||
=========================== */
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::{
|
||||
command::CommandBuilder,
|
||||
executors::{ExecutorError, StandardCodingAgentExecutor},
|
||||
logs::{
|
||||
ActionType, EditDiff, NormalizedEntry, NormalizedEntryType,
|
||||
ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, TodoItem,
|
||||
plain_text_processor::{MessageBoundary, PlainTextLogProcessor},
|
||||
utils::EntryIndexProvider,
|
||||
},
|
||||
@@ -724,8 +724,20 @@ impl ToolUtils {
|
||||
Tool::WebFetch { url, .. } => ActionType::Other {
|
||||
description: format!("Web fetch: {url}"),
|
||||
},
|
||||
Tool::TodoWrite { .. } | Tool::TodoRead => ActionType::Other {
|
||||
description: "TODO list management".to_string(),
|
||||
Tool::TodoWrite { todos } => ActionType::TodoManagement {
|
||||
todos: todos
|
||||
.iter()
|
||||
.map(|t| TodoItem {
|
||||
content: t.content.clone(),
|
||||
status: t.status.clone(),
|
||||
priority: t.priority.clone(),
|
||||
})
|
||||
.collect(),
|
||||
operation: "write".to_string(),
|
||||
},
|
||||
Tool::TodoRead => ActionType::TodoManagement {
|
||||
todos: vec![],
|
||||
operation: "read".to_string(),
|
||||
},
|
||||
Tool::Task { description } => ActionType::Other {
|
||||
description: format!("Task: {description}"),
|
||||
@@ -787,11 +799,11 @@ impl ToolUtils {
|
||||
Tool::WebFetch { url, .. } => {
|
||||
format!("fetch `{url}`")
|
||||
}
|
||||
Tool::TodoWrite { todos } => Self::generate_todo_content(todos),
|
||||
Tool::TodoRead => "Managing TODO list".to_string(),
|
||||
Tool::Task { description } => {
|
||||
format!("Task: `{description}`")
|
||||
}
|
||||
Tool::TodoWrite { .. } => "TODO list updated".to_string(),
|
||||
Tool::TodoRead => "TODO list read".to_string(),
|
||||
Tool::Other { tool_name, .. } => {
|
||||
// Handle MCP tools (format: client_name_tool_name)
|
||||
if tool_name.contains('_') {
|
||||
@@ -802,26 +814,6 @@ impl ToolUtils {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate formatted content for TODO tools from TodoInfo struct
|
||||
fn generate_todo_content(todos: &[TodoInfo]) -> String {
|
||||
if todos.is_empty() {
|
||||
return "Managing TODO list".to_string();
|
||||
}
|
||||
|
||||
let mut todo_items = Vec::new();
|
||||
for todo in todos {
|
||||
let status_emoji = match todo.status.as_str() {
|
||||
"completed" => "✅",
|
||||
"in_progress" => "🔄",
|
||||
"pending" | "todo" => "⏳",
|
||||
_ => "📝",
|
||||
};
|
||||
let priority = todo.priority.as_deref().unwrap_or("medium");
|
||||
todo_items.push(format!("{} {} ({})", status_emoji, todo.content, priority));
|
||||
}
|
||||
format!("TODO List:\n{}", todo_items.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -45,16 +45,47 @@ pub struct NormalizedEntry {
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct TodoItem {
|
||||
pub content: String,
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub priority: Option<String>,
|
||||
}
|
||||
|
||||
/// Types of tool actions that can be performed
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "action", rename_all = "snake_case")]
|
||||
pub enum ActionType {
|
||||
FileRead { path: String },
|
||||
FileEdit { path: String, diffs: Vec<EditDiff> },
|
||||
CommandRun { command: String },
|
||||
Search { query: String },
|
||||
WebFetch { url: String },
|
||||
TaskCreate { description: String },
|
||||
PlanPresentation { plan: String },
|
||||
Other { description: String },
|
||||
FileRead {
|
||||
path: String,
|
||||
},
|
||||
FileEdit {
|
||||
path: String,
|
||||
diffs: Vec<EditDiff>,
|
||||
},
|
||||
CommandRun {
|
||||
command: String,
|
||||
},
|
||||
Search {
|
||||
query: String,
|
||||
},
|
||||
WebFetch {
|
||||
url: String,
|
||||
},
|
||||
TaskCreate {
|
||||
description: String,
|
||||
},
|
||||
PlanPresentation {
|
||||
plan: String,
|
||||
},
|
||||
TodoManagement {
|
||||
todos: Vec<TodoItem>,
|
||||
operation: String,
|
||||
},
|
||||
Other {
|
||||
description: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ fn generate_types_content() -> String {
|
||||
executors::logs::NormalizedEntryType::decl(),
|
||||
executors::logs::EditDiff::decl(),
|
||||
executors::logs::ActionType::decl(),
|
||||
executors::logs::TodoItem::decl(),
|
||||
executors::logs::utils::patch::PatchType::decl(),
|
||||
serde_json::Value::decl(),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user