Skip to content

Commit a9501e7

Browse files
author
kiran-garre
committed
feat: Add add and remove functionality for to-do lists
1 parent 302cecd commit a9501e7

File tree

4 files changed

+178
-45
lines changed

4 files changed

+178
-45
lines changed

crates/chat-cli/src/cli/chat/cli/todos.rs

Lines changed: 62 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ pub enum TodoSubcommand {
2828

2929
/// View a to-do list
3030
View,
31+
32+
/// Delete a to-do list
33+
Delete,
3134
}
3235

3336
/// Used for displaying completed and in-progress todo lists
@@ -61,7 +64,7 @@ impl TodoSubcommand {
6164
Self::Show => match Self::get_descriptions_and_statuses(os) {
6265
Ok(entries) => {
6366
if entries.is_empty() {
64-
execute!(session.stderr, style::Print("No to-do lists to show\n"),)?;
67+
execute!(session.stderr, style::Print("No to-do lists to show!\n"),)?;
6568
}
6669
for e in entries {
6770
execute!(session.stderr, style::Print(e), style::Print("\n"),)?;
@@ -96,58 +99,39 @@ impl TodoSubcommand {
9699
style::Print("✔ Cleared finished to-do lists!\n".green())
97100
)?;
98101
} else {
99-
execute!(
100-
session.stderr,
101-
style::Print("No finished to-do lists to clear!\n".green())
102-
)?;
102+
execute!(session.stderr, style::Print("No finished to-do lists to clear!\n"))?;
103103
}
104104
},
105-
Self::Resume => {
106-
match Self::get_descriptions_and_statuses(os) {
107-
Ok(entries) => {
108-
if entries.is_empty() {
109-
execute!(session.stderr, style::Print("No to-do lists to show\n"),)?;
110-
} else {
111-
let selection = FuzzySelect::new()
112-
.with_prompt("Select a to-do list to resume:")
113-
.items(&entries)
114-
.report(false)
115-
.interact_opt()
116-
.unwrap_or(None);
117-
118-
if let Some(index) = selection {
119-
if index < entries.len() {
120-
execute!(
121-
session.stderr,
122-
style::Print("⟳ Resuming: ".magenta()),
123-
style::Print(format!("{}\n", entries[index].description.clone())),
124-
)?;
125-
return session.resume_todo(os, &entries[index].id).await;
126-
}
105+
Self::Resume => match Self::get_descriptions_and_statuses(os) {
106+
Ok(entries) => {
107+
if entries.is_empty() {
108+
execute!(session.stderr, style::Print("No to-do lists to resume!\n"),)?;
109+
} else {
110+
if let Some(index) = fuzzy_select_todos(&entries, "Select a to-do list to resume:") {
111+
if index < entries.len() {
112+
execute!(
113+
session.stderr,
114+
style::Print("⟳ Resuming: ".magenta()),
115+
style::Print(format!("{}\n", entries[index].description.clone())),
116+
)?;
117+
return session.resume_todo(os, &entries[index].id).await;
127118
}
128119
}
129-
},
130-
Err(_) => return Err(ChatError::Custom("Could not show to-do lists".into())),
131-
};
120+
}
121+
},
122+
Err(_) => return Err(ChatError::Custom("Could not show to-do lists".into())),
132123
},
133124
Self::View => match Self::get_descriptions_and_statuses(os) {
134125
Ok(entries) => {
135126
if entries.is_empty() {
136-
execute!(session.stderr, style::Print("No to-do lists to view\n"))?;
127+
execute!(session.stderr, style::Print("No to-do lists to view!\n"))?;
137128
} else {
138-
let selection = FuzzySelect::new()
139-
.with_prompt("Select a to-do list to view:")
140-
.items(&entries)
141-
.report(false)
142-
.interact_opt()
143-
.unwrap_or(None);
144-
145-
if let Some(index) = selection {
129+
if let Some(index) = fuzzy_select_todos(&entries, "Select a to-do list to view:") {
146130
if index < entries.len() {
147131
let list = match TodoState::load(os, &entries[index].id) {
148132
Ok(list) => list,
149133
Err(_) => {
150-
return Err(ChatError::Custom("Could not load requested to-do list".into()));
134+
return Err(ChatError::Custom("Could not load the selected to-do list".into()));
151135
},
152136
};
153137
execute!(
@@ -161,7 +145,9 @@ impl TodoSubcommand {
161145
match list.display_list(&mut session.stderr) {
162146
Ok(_) => {},
163147
Err(_) => {
164-
return Err(ChatError::Custom("Could not display requested to-do list".into()));
148+
return Err(ChatError::Custom(
149+
"Could not display the selected to-do list".into(),
150+
));
165151
},
166152
};
167153
execute!(session.stderr, style::Print("\n"),)?;
@@ -171,6 +157,32 @@ impl TodoSubcommand {
171157
},
172158
Err(_) => return Err(ChatError::Custom("Could not show to-do lists".into())),
173159
},
160+
Self::Delete => match Self::get_descriptions_and_statuses(os) {
161+
Ok(entries) => {
162+
if entries.is_empty() {
163+
execute!(session.stderr, style::Print("No to-do lists to delete!\n"))?;
164+
} else {
165+
if let Some(index) = fuzzy_select_todos(&entries, "Select a to-do list to delete:") {
166+
if index < entries.len() {
167+
match os.database.delete_todo(&entries[index].id) {
168+
Ok(_) => {},
169+
Err(_) => {
170+
return Err(ChatError::Custom(
171+
"Could not delete the selected to-do list".into(),
172+
));
173+
},
174+
};
175+
execute!(
176+
session.stderr,
177+
style::Print("✔ Deleted to-do list: ".green()),
178+
style::Print(format!("{}\n", entries[index].description.clone().dark_grey()))
179+
)?;
180+
}
181+
}
182+
}
183+
},
184+
Err(_) => return Err(ChatError::Custom("Could not show to-do lists".into())),
185+
},
174186
}
175187
Ok(ChatState::PromptUser {
176188
skip_printing_tools: true,
@@ -208,6 +220,15 @@ impl TodoSubcommand {
208220
}
209221
}
210222

223+
fn fuzzy_select_todos(entries: &Vec<TodoDisplayEntry>, prompt_str: &str) -> Option<usize> {
224+
FuzzySelect::new()
225+
.with_prompt(prompt_str)
226+
.items(&entries)
227+
.report(false)
228+
.interact_opt()
229+
.unwrap_or(None)
230+
}
231+
211232
// const MAX_LINE_LENGTH: usize = 80;
212233

213234
// // FIX: Hacky workaround for cleanly wrapping lines

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,6 @@ impl ChatSession {
988988
},
989989
};
990990

991-
// Use the normal sendable conversation state which includes tool definitions
992991
let conv_state = self
993992
.conversation
994993
.as_sendable_conversation_state(os, &mut self.stderr, false)

crates/chat-cli/src/cli/chat/tools/todo.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::io::Write;
2+
use std::collections::HashSet;
23
use std::time::{
34
SystemTime,
45
UNIX_EPOCH,
@@ -45,6 +46,19 @@ pub enum TodoInput {
4546

4647
#[serde(rename = "load")]
4748
Load { id: String },
49+
50+
#[serde(rename = "add")]
51+
Add {
52+
new_tasks: Vec<String>,
53+
insert_indices: Vec<usize>,
54+
new_description: Option<String>,
55+
},
56+
57+
#[serde(rename = "remove")]
58+
Remove {
59+
remove_indices: Vec<usize>,
60+
new_description: Option<String>,
61+
}
4862
}
4963

5064
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
@@ -174,6 +188,38 @@ impl TodoInput {
174188
TodoState::set_current_todo_id(os, id)?;
175189
TodoState::load(os, id)?
176190
},
191+
TodoInput::Add { new_tasks, insert_indices, new_description } => {
192+
let current_id = match TodoState::get_current_todo_id(os)? {
193+
Some(id) => id,
194+
None => bail!("No to-do list currently loaded"),
195+
};
196+
let mut state = TodoState::load(os, &current_id)?;
197+
for (i, task) in insert_indices.iter().zip(new_tasks.iter()) {
198+
state.tasks.insert(*i, task.clone());
199+
state.completed.insert(*i, false);
200+
}
201+
if let Some(description) = new_description {
202+
state.task_description = description.clone();
203+
}
204+
state.save(os, &current_id)?;
205+
state
206+
},
207+
TodoInput::Remove { remove_indices, new_description } => {
208+
let current_id = match TodoState::get_current_todo_id(os)? {
209+
Some(id) => id,
210+
None => bail!("No to-do list currently loaded"),
211+
};
212+
let mut state = TodoState::load(os, &current_id)?;
213+
for i in remove_indices.iter() {
214+
state.tasks.remove(*i);
215+
state.completed.remove(*i);
216+
}
217+
if let Some(description) = new_description {
218+
state.task_description = description.clone();
219+
}
220+
state.save(os, &current_id)?;
221+
state
222+
}
177223
};
178224
state.display_list(output)?;
179225
output.flush()?;
@@ -221,7 +267,49 @@ impl TodoInput {
221267
bail!("Loaded todo list is empty");
222268
}
223269
},
270+
TodoInput::Add { new_tasks, insert_indices, new_description } => {
271+
let current_id = match TodoState::get_current_todo_id(os)? {
272+
Some(id) => id,
273+
None => bail!("No todo list is currently loaded"),
274+
};
275+
let state = TodoState::load(os, &current_id)?;
276+
if new_tasks.iter().any(|task| task.trim().is_empty()) {
277+
bail!("New tasks cannot be empty");
278+
} else if has_duplicates(&insert_indices) {
279+
bail!("Insertion indices must be unique")
280+
} else if new_tasks.len() != insert_indices.len() {
281+
bail!("Must provide an index for every new task");
282+
} else if insert_indices.iter().any(|i| *i > state.tasks.len()) {
283+
bail!("Index is out of bounds");
284+
} else if new_description.is_some() && new_description.as_ref().unwrap().trim().is_empty() {
285+
bail!("New description cannot be empty");
286+
}
287+
},
288+
TodoInput::Remove { remove_indices, new_description } => {
289+
let current_id = match TodoState::get_current_todo_id(os)? {
290+
Some(id) => id,
291+
None => bail!("No todo list is currently loaded"),
292+
};
293+
let state = TodoState::load(os, &current_id)?;
294+
if has_duplicates(&remove_indices) {
295+
bail!("Removal indices must be unique")
296+
} else if remove_indices.iter().any(|i| *i > state.tasks.len()) {
297+
bail!("Index is out of bounds");
298+
} else if new_description.is_some() && new_description.as_ref().unwrap().trim().is_empty() {
299+
bail!("New description cannot be empty");
300+
}
301+
}
224302
}
225303
Ok(())
226304
}
227305
}
306+
307+
308+
/// Generated by Q
309+
fn has_duplicates<T>(vec: &[T]) -> bool
310+
where
311+
T: std::hash::Hash + Eq,
312+
{
313+
let mut seen = HashSet::with_capacity(vec.len());
314+
vec.iter().any(|item| !seen.insert(item))
315+
}

crates/chat-cli/src/cli/chat/tools/tool_index.json

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,13 @@
235235
},
236236
"todo_list": {
237237
"name": "todo_list",
238-
"description": "A tool for creating a TODO list and keeping track of tasks. This tool should be requested EVERY time the user gives you a task that will take multiple steps. A TODO list should be made BEFORE executing any steps. Steps should be marked off AS YOU COMPLETE THEM. DO NOT display your own tasks or todo list AT ANY POINT; this is done for you. Complete the tasks in the same order that you provide them.",
238+
"description": "A tool for creating a TODO list and keeping track of tasks. This tool should be requested EVERY time the user gives you a task that will take multiple steps. A TODO list should be made BEFORE executing any steps. Steps should be marked off AS YOU COMPLETE THEM. DO NOT display your own tasks or todo list AT ANY POINT; this is done for you. Complete the tasks in the same order that you provide them. If the user tells you to skip a step, DO NOT mark it as completed.",
239239
"input_schema": {
240240
"type": "object",
241241
"properties": {
242242
"command": {
243243
"type": "string",
244-
"enum": ["create", "complete", "load"],
244+
"enum": ["create", "complete", "load", "add", "remove"],
245245
"description": "The command to run. Allowed options are `add`, `complete`."
246246
},
247247
"tasks": {
@@ -256,7 +256,7 @@
256256
"type": "string"
257257
},
258258
"completed_indices": {
259-
"description": "Required parameter of `complete` command containing the 0-INDEXED numbers of EVERY completed task. Each task should be marked as completed IMMEDIATELY after it is finished. DO NOT mark tasks as completed if you skip them.",
259+
"description": "Required parameter of `complete` command containing the 0-INDEXED numbers of EVERY completed task. Each task should be marked as completed IMMEDIATELY after it is finished.",
260260
"type": "array",
261261
"items": {
262262
"type": "integer"
@@ -276,6 +276,31 @@
276276
"id": {
277277
"description": "Required parameter of `load` command containing ID of todo list to load",
278278
"type": "string"
279+
},
280+
"new_tasks": {
281+
"description": "Required parameter of `add` command containing a list of new tasks to be added to the to-do list.",
282+
"type": "array",
283+
"items": {
284+
"type": "string"
285+
}
286+
},
287+
"insert_indices": {
288+
"description": "Required parameter of `add` command containing a list of 0-INDEXED positions to insert the new tasks. There MUST be an index for every new task being added.",
289+
"type": "array",
290+
"items": {
291+
"type": "integer"
292+
}
293+
},
294+
"new_description": {
295+
"description": "Optional parameter of `add` and `remove` containing a new task description. Use this when the updated set of tasks significantly change the goal or overall procedure of the task.",
296+
"type": "string"
297+
},
298+
"remove_indices": {
299+
"description": "Required parameter of `remove` command containing a list of 0-INDEXED positions of tasks to remove.",
300+
"type": "array",
301+
"items": {
302+
"type": "integer"
303+
}
279304
}
280305
},
281306
"required": ["command"]

0 commit comments

Comments
 (0)