Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
264c308
feat: update build scripts to build qchat (#2198)
brandonskiser Jul 1, 2025
415624a
feat: Add basic todo list tool
Jul 3, 2025
30fc467
Slash command + persistence in progress commit
Jul 8, 2025
da54512
feat: Add basic persistence functionality to todo_list
Jul 8, 2025
2a09ea9
Add append-only context and "modified files" fields to "complete" com…
Jul 9, 2025
398bdb4
refactor: Change to-do lists to use database over filesystem
Jul 9, 2025
c000ffb
feat: Add `view` subcommand for /todos
Jul 10, 2025
302cecd
feat: Add clear-finished subcommand for /todos
Jul 10, 2025
a9501e7
feat: Add `add` and `remove` functionality for to-do lists
Jul 10, 2025
814a96a
fix: Add feedback after calling todo_list commands
Jul 14, 2025
216ce00
feat: Cleaned up todo list UX and removed /todos show
Jul 15, 2025
714474a
feat: Add /todos show
Jul 15, 2025
06837f2
trying to merge changes
Aug 7, 2025
1a4e7e1
fix: Fix all merge conflicts, back to working state
Aug 7, 2025
3299478
chore: Resolve merge conflicts, run formatter and clippy
Aug 8, 2025
e31c7ed
chore: Fix real clippy errors (hopefully)
Aug 8, 2025
058cc62
Merge branch 'main' of github.com:aws/amazon-q-developer-cli into kir…
Aug 8, 2025
12a70b5
chore: Run formatter
Aug 8, 2025
ef44517
chore: Remove old/unrelated files
Aug 8, 2025
cbbfa20
chore: Remove old/unrelated files again
Aug 8, 2025
5ebf87a
refactor: Several changes made to `todo_list` tool
Aug 11, 2025
5e76bc1
chore: Fix typo in tool_index.json
Aug 11, 2025
7f91788
chore: Remove buildspec files
Aug 11, 2025
70423f9
chore: Edit buildspec files to be identical to main
Aug 11, 2025
81b1dac
chore: Remove debugging in todos.rs
Aug 11, 2025
e7ea8af
chore: Merging changes from main
Aug 12, 2025
d7aa647
refactor: Convert from global database store to per-directory filesys…
Aug 12, 2025
ee6b932
merge commit
Aug 13, 2025
6d00e0f
Merge branch 'main' into kiran-garre/todo-list
Aug 13, 2025
4b684f9
fix: Update dependencies to fix slab issue
Aug 13, 2025
f2fc3d1
chore: Change variable names, add comments, add file extension to tod…
Aug 13, 2025
2fcbe56
Merge commit
Aug 13, 2025
9e0b732
Removed merge markers from Cargo.lock
Aug 13, 2025
030c79a
Fixed Cargo.lock
Aug 13, 2025
cbc8625
fix: Modify system prompt to contain current todo list id
Aug 14, 2025
2bf1c40
Merge branch 'main' of github.com:aws/amazon-q-developer-cli
Aug 14, 2025
2d9c3b4
Merge branch 'main' into kiran-garre/todo-list
Aug 14, 2025
3e72ee8
fix: Correctly display command options in tool_index.json
Aug 21, 2025
1be09da
Match Cargo.lock to main
Aug 21, 2025
7f71a68
Merge branch 'main' of github.com:aws/amazon-q-developer-cli into kir…
Aug 27, 2025
d2a8936
chore: Run formatter
Aug 27, 2025
b26c006
refactor: Consolidate task logic, parameterize local directory name
Aug 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
717 changes: 395 additions & 322 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion build-config/buildspec-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,3 @@ artifacts:
# Signatures
- ./*.asc
- ./*.sig

1 change: 0 additions & 1 deletion build-config/buildspec-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,3 @@ artifacts:
- ./*.zip
# Hashes
- ./*.sha256

7 changes: 7 additions & 0 deletions crates/chat-cli/src/cli/chat/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod persist;
pub mod profile;
pub mod prompts;
pub mod subscribe;
pub mod todos;
pub mod tools;
pub mod usage;

Expand All @@ -25,6 +26,7 @@ use model::ModelArgs;
use persist::PersistSubcommand;
use profile::AgentSubcommand;
use prompts::PromptsArgs;
use todos::TodoSubcommand;
use tools::ToolsArgs;

use crate::cli::chat::cli::subscribe::SubscribeArgs;
Expand Down Expand Up @@ -85,6 +87,9 @@ pub enum SlashCommand {
Persist(PersistSubcommand),
// #[command(flatten)]
// Root(RootSubcommand),
/// View, manage, and resume to-do lists
#[command(subcommand)]
Todos(TodoSubcommand),
}

impl SlashCommand {
Expand Down Expand Up @@ -146,6 +151,7 @@ impl SlashCommand {
// skip_printing_tools: true,
// })
// },
Self::Todos(subcommand) => subcommand.execute(os, session).await,
}
}

Expand All @@ -171,6 +177,7 @@ impl SlashCommand {
PersistSubcommand::Save { .. } => "save",
PersistSubcommand::Load { .. } => "load",
},
Self::Todos(_) => "todos",
}
}

Expand Down
197 changes: 197 additions & 0 deletions crates/chat-cli/src/cli/chat/cli/todos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
use clap::Subcommand;
use crossterm::execute;
use crossterm::style::{
self,
Stylize,
};
use dialoguer::FuzzySelect;
use eyre::Result;

use crate::cli::chat::tools::todo::TodoState;
use crate::cli::chat::{
ChatError,
ChatSession,
ChatState,
};
use crate::os::Os;

#[derive(Debug, PartialEq, Subcommand)]
pub enum TodoSubcommand {
/// Delete all completed to-do lists
ClearFinished,

/// Resume a selected to-do list
Resume,

/// View a to-do list
View,

/// Delete a to-do list
Delete {
#[arg(long, short)]
all: bool,
},
}

/// Used for displaying completed and in-progress todo lists
pub struct TodoDisplayEntry {
pub num_completed: usize,
pub num_tasks: usize,
pub description: String,
pub id: String,
}

impl std::fmt::Display for TodoDisplayEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.num_completed == self.num_tasks {
write!(f, "{} {}", "✓".green().bold(), self.description.clone(),)
} else {
write!(
f,
"{} {} ({}/{})",
"✗".red().bold(),
self.description.clone(),
self.num_completed,
self.num_tasks
)
}
}
}

impl TodoSubcommand {
pub async fn execute(self, os: &mut Os, session: &mut ChatSession) -> Result<ChatState, ChatError> {
match self {
Self::ClearFinished => {
let (todos, errors) = match TodoState::get_all_todos(os).await {
Ok(res) => res,
Err(e) => return Err(ChatError::Custom(format!("Could not get to-do lists: {e}").into())),
};
let mut cleared_one = false;

for todo_status in todos.iter() {
if todo_status.completed.iter().all(|b| *b) {
match TodoState::delete_todo(os, &todo_status.id).await {
Ok(_) => cleared_one = true,
Err(e) => {
return Err(ChatError::Custom(format!("Could not delete to-do list: {e}").into()));
},
};
}
}
if cleared_one {
execute!(
session.stderr,
style::Print("✔ Cleared finished to-do lists!\n".green())
)?;
} else {
execute!(session.stderr, style::Print("No finished to-do lists to clear!\n"))?;
}
if !errors.is_empty() {
execute!(
session.stderr,
style::Print(format!("* Failed to get {} todo list(s)\n", errors.len()).dark_grey())
)?;
}
},
Self::Resume => match Self::get_descriptions_and_statuses(os).await {
Ok(entries) => {
if entries.is_empty() {
execute!(session.stderr, style::Print("No to-do lists to resume!\n"),)?;
} else if let Some(index) = fuzzy_select_todos(&entries, "Select a to-do list to resume:") {
if index < entries.len() {
execute!(
session.stderr,
style::Print(format!(
"{} {}",
"⟳ Resuming:".magenta(),
entries[index].description.clone()
))
)?;
return session.resume_todo_request(os, &entries[index].id).await;
}
}
},
Err(e) => return Err(ChatError::Custom(format!("Could not show to-do lists: {e}").into())),
},
Self::View => match Self::get_descriptions_and_statuses(os).await {
Ok(entries) => {
if entries.is_empty() {
execute!(session.stderr, style::Print("No to-do lists to view!\n"))?;
} else if let Some(index) = fuzzy_select_todos(&entries, "Select a to-do list to view:") {
if index < entries.len() {
let list = TodoState::load(os, &entries[index].id).await.map_err(|e| {
ChatError::Custom(format!("Could not load current to-do list: {e}").into())
})?;
execute!(
session.stderr,
style::Print(format!(
"{} {}\n\n",
"Viewing:".magenta(),
entries[index].description.clone()
))
)?;
if list.display_list(&mut session.stderr).is_err() {
return Err(ChatError::Custom("Could not display the selected to-do list".into()));
}
execute!(session.stderr, style::Print("\n"),)?;
}
}
},
Err(e) => return Err(ChatError::Custom(format!("Could not show to-do lists: {e}").into())),
},
Self::Delete { all } => match Self::get_descriptions_and_statuses(os).await {
Ok(entries) => {
if entries.is_empty() {
execute!(session.stderr, style::Print("No to-do lists to delete!\n"))?;
} else if all {
for entry in entries {
TodoState::delete_todo(os, &entry.id)
.await
.map_err(|_e| ChatError::Custom("Could not delete all to-do lists".into()))?;
}
execute!(session.stderr, style::Print("✔ Deleted all to-do lists!\n".green()),)?;
} else if let Some(index) = fuzzy_select_todos(&entries, "Select a to-do list to delete:") {
if index < entries.len() {
TodoState::delete_todo(os, &entries[index].id).await.map_err(|e| {
ChatError::Custom(format!("Could not delete the selected to-do list: {e}").into())
})?;
execute!(
session.stderr,
style::Print("✔ Deleted to-do list: ".green()),
style::Print(format!("{}\n", entries[index].description.clone().dark_grey()))
)?;
}
}
},
Err(e) => return Err(ChatError::Custom(format!("Could not show to-do lists: {e}").into())),
},
}
Ok(ChatState::PromptUser {
skip_printing_tools: true,
})
}

/// Convert all to-do list state entries to displayable entries
async fn get_descriptions_and_statuses(os: &Os) -> Result<Vec<TodoDisplayEntry>> {
let mut out = Vec::new();
let (todos, _) = TodoState::get_all_todos(os).await?;
for todo in todos.iter() {
out.push(TodoDisplayEntry {
num_completed: todo.completed.iter().filter(|b| **b).count(),
num_tasks: todo.completed.len(),
description: todo.description.clone(),
id: todo.id.clone(),
});
}
Ok(out)
}
}

fn fuzzy_select_todos(entries: &[TodoDisplayEntry], prompt_str: &str) -> Option<usize> {
FuzzySelect::new()
.with_prompt(prompt_str)
.items(entries)
.report(false)
.interact_opt()
.unwrap_or(None)
}
1 change: 1 addition & 0 deletions crates/chat-cli/src/cli/chat/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crossterm::{
execute,
style,
};
use eyre::Result;
use serde::{
Deserialize,
Serialize,
Expand Down
57 changes: 56 additions & 1 deletion crates/chat-cli/src/cli/chat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,15 @@ use crate::api_client::{
};
use crate::auth::AuthError;
use crate::auth::builder_id::is_idc_user;
use crate::cli::TodoState;
use crate::cli::agent::Agents;
use crate::cli::chat::cli::SlashCommand;
use crate::cli::chat::cli::prompts::{
GetPromptError,
PromptsSubcommand,
};
use crate::cli::chat::message::UserMessage;
use crate::cli::chat::tools::ToolOrigin;
use crate::cli::chat::util::sanitize_unicode_tags;
use crate::database::settings::Setting;
use crate::mcp_client::Prompt;
Expand Down Expand Up @@ -649,6 +652,11 @@ impl ChatSession {
}
});

// Create for cleaner error handling for todo lists
// This is more of a convenience thing but is not required, so the Result
// is ignored
let _ = TodoState::init_dir(os).await;

Ok(Self {
stdout,
stderr,
Expand Down Expand Up @@ -2788,6 +2796,53 @@ impl ChatSession {
tracing::warn!("Failed to send slash command telemetry: {}", e);
}
}

/// Prompts Q to resume a to-do list with the given id by calling the load
/// command of the todo_list tool
pub async fn resume_todo_request(&mut self, os: &mut Os, id: &str) -> Result<ChatState, ChatError> {
// Have to unpack each value separately since Reports can't be converted to
// ChatError
let todo_list = match TodoState::load(os, id).await {
Ok(todo) => todo,
Err(e) => {
return Err(ChatError::Custom(format!("Error getting todo list: {e}").into()));
},
};
let contents = match serde_json::to_string(&todo_list) {
Ok(s) => s,
Err(e) => return Err(ChatError::Custom(format!("Error deserializing todo list: {e}").into())),
};
let summary_content = format!(
"[SYSTEM NOTE: This is an automated request, not from the user]\n
Read the TODO list contents below and understand the task description, completed tasks, and provided context.\n
Call the `load` command of the todo_list tool with the given ID as an argument to display the TODO list to the user and officially resume execution of the TODO list tasks.\n
You do not need to display the tasks to the user yourself. You can begin completing the tasks after calling the `load` command.\n
TODO LIST CONTENTS: {}\n
ID: {}\n",
contents,
id
);

let summary_message = UserMessage::new_prompt(summary_content.clone(), None);

// Only send the todo_list tool
let mut tools = self.conversation.tools.clone();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still sends the entire tool spec, you'd have to construct a special request for this

Here is a reference for how the compaction request is created - https://github.com/aws/amazon-q-developer-cli/blob/main/crates/chat-cli/src/cli/chat/conversation.rs#L528

I think you can then switch to ChatState::HandleResponseStream with this conversation state.

Also be sure to call ChatSession::reset_user_turn for this as well

tools.retain(|k, v| match k {
ToolOrigin::Native => {
v.retain(|tool| match tool {
api_client::model::Tool::ToolSpecification(tool_spec) => tool_spec.name == "todo_list",
});
true
},
ToolOrigin::McpServer(_) => false,
});

Ok(ChatState::HandleInput {
input: summary_message
.into_user_input_message(self.conversation.model.clone(), &tools)
.content,
})
}
}

/// Replaces amzn_codewhisperer_client::types::SubscriptionStatus with a more descriptive type.
Expand Down Expand Up @@ -2848,7 +2903,7 @@ async fn get_subscription_status_with_spinner(
.await;
}

async fn with_spinner<T, E, F, Fut>(output: &mut impl std::io::Write, spinner_text: &str, f: F) -> Result<T, E>
pub async fn with_spinner<T, E, F, Fut>(output: &mut impl std::io::Write, spinner_text: &str, f: F) -> Result<T, E>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<T, E>>,
Expand Down
2 changes: 2 additions & 0 deletions crates/chat-cli/src/cli/chat/tool_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ use crate::cli::chat::tools::fs_write::FsWrite;
use crate::cli::chat::tools::gh_issue::GhIssue;
use crate::cli::chat::tools::knowledge::Knowledge;
use crate::cli::chat::tools::thinking::Thinking;
use crate::cli::chat::tools::todo::TodoInput;
use crate::cli::chat::tools::use_aws::UseAws;
use crate::cli::chat::tools::{
Tool,
Expand Down Expand Up @@ -1066,6 +1067,7 @@ impl ToolManager {
"report_issue" => Tool::GhIssue(serde_json::from_value::<GhIssue>(value.args).map_err(map_err)?),
"thinking" => Tool::Thinking(serde_json::from_value::<Thinking>(value.args).map_err(map_err)?),
"knowledge" => Tool::Knowledge(serde_json::from_value::<Knowledge>(value.args).map_err(map_err)?),
"todo_list" => Tool::Todo(serde_json::from_value::<TodoInput>(value.args).map_err(map_err)?),
// Note that this name is namespaced with server_name{DELIMITER}tool_name
name => {
// Note: tn_map also has tools that underwent no transformation. In otherwords, if
Expand Down
Loading
Loading