diff --git a/.github/workflows/docs-generator.yaml b/.github/workflows/docs-generator.yaml new file mode 100644 index 0000000000..f8a50902de --- /dev/null +++ b/.github/workflows/docs-generator.yaml @@ -0,0 +1,65 @@ +name: docs-generator + +on: + # workflow_dispatch: + # inputs: + # pr_number: + # description: 'Number of PR to document' + # required: true + # type: string + push: + +jobs: + generate_docs: + runs-on: ubuntu-latest + env: + AMAZON_Q_SIGV4: 1 + CHAT_DOWNLOAD_ROLE_ARN: ${{ secrets.CHAT_DOWNLOAD_ROLE_ARN }} + CHAT_BUILD_BUCKET_NAME: ${{ secrets.CHAT_BUILD_BUCKET_NAME }} + PR_FILE: "pr-contents.txt" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_HASH: "latest" + TEST_PR_NUMBER: 2533 + permissions: + id-token: write + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Fetch main branch + run: git fetch origin main:main + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_TB_ROLE }} + aws-region: us-east-1 + + - name: Make scripts executable + run: | + chmod +x docs-generation/setup_amazon_q.sh + chmod +x docs-generation/create-docs-pr.sh + chmod +x docs-generation/read-pr.sh + chmod +x docs-generation/update-docs.sh + + - name: Run setup script + run: bash docs-generation/setup_amazon_q.sh + + - name: Generate PR contents file + run: bash docs-generation/read-pr.sh ${{ env.TEST_PR_NUMBER }} + + - name: Update docs + run: bash docs-generation/update-docs.sh + + - name: Create PR + if: success() + run: bash docs-generation/create-docs-pr.sh ${{ env.TEST_PR_NUMBER }} + + + + + + \ No newline at end of file diff --git a/docs-generation/create-docs-pr.sh b/docs-generation/create-docs-pr.sh new file mode 100755 index 0000000000..fc9b01a44c --- /dev/null +++ b/docs-generation/create-docs-pr.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +PR_NUMBER=$1 +BRANCH_NAME="docs-update-for-pr-$PR_NUMBER" + +# Ensure we have changes to merge +if [ -z "$(git status --porcelain)" ]; then + echo "No changes to commit" + exit 0 +fi + +git config user.name "docs-generator[bot]" +git config user.email "docs-generator[bot]@amazon.com" + +# Create branch and push +git checkout -b "$BRANCH_NAME" +git add . +git commit -m "Update docs based on PR #$PR_NUMBER + +Auto-generated by Q" + +git push origin "$BRANCH_NAME" + +# Create PR +gh pr create \ + --title "Update docs based on PR #$PR_NUMBER" \ + --body "Auto-generated documentation updates based on changes in PR #$PR_NUMBER" \ + --base main \ + --head "$BRANCH_NAME" diff --git a/docs-generation/read-pr.sh b/docs-generation/read-pr.sh new file mode 100755 index 0000000000..217c7aa9e0 --- /dev/null +++ b/docs-generation/read-pr.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +PR_NUMBER=$1 + +# Add PR information +echo "====== PR Information ======\n" > $PR_FILE +gh pr view $PR_NUMBER --json title,body --jq '"Title: " + .title + "\nDescription: " + .body' >> $PR_FILE + +# Include PR diffs +echo -e "\n====== PR Diffs ======\n" >> $PR_FILE +gh pr diff $PR_NUMBER >> $PR_FILE + + + + + + \ No newline at end of file diff --git a/docs-generation/setup_amazon_q.sh b/docs-generation/setup_amazon_q.sh new file mode 100755 index 0000000000..e5c2c923ee --- /dev/null +++ b/docs-generation/setup_amazon_q.sh @@ -0,0 +1,54 @@ +#!/bin/bash +set -e +# if git hash empty then set to latest auto +sudo apt-get update +sudo apt-get install -y curl wget unzip jq + +# Create AWS credentials from environment variables +mkdir -p ~/.aws +cat > ~/.aws/credentials << EOF +[default] +aws_access_key_id = ${AWS_ACCESS_KEY_ID} +aws_secret_access_key = ${AWS_SECRET_ACCESS_KEY} +aws_session_token = ${AWS_SESSION_TOKEN} +EOF +chmod 600 ~/.aws/credentials + +cat > ~/.aws/config << EOF +[default] +region = us-east-1 +EOF +chmod 600 ~/.aws/config + +# Assume role and capture temporary credentials --> needed for s3 bucket access for build +echo "Assuming AWS s3 role" +TEMP_CREDENTIALS=$(aws sts assume-role --role-arn ${CHAT_DOWNLOAD_ROLE_ARN} --role-session-name S3AccessSession 2>/dev/null || echo '{}') +QCHAT_ACCESSKEY=$(echo $TEMP_CREDENTIALS | jq -r '.Credentials.AccessKeyId') +Q_SECRET_ACCESS_KEY=$(echo $TEMP_CREDENTIALS | jq -r '.Credentials.SecretAccessKey') +Q_SESSION_TOKEN=$(echo $TEMP_CREDENTIALS | jq -r '.Credentials.SessionToken') + +# Download specific build from S3 based on commit hash +echo "Downloading Amazon Q CLI build from S3..." +S3_PREFIX="main/${GIT_HASH}/x86_64-unknown-linux-musl" +echo "Downloading qchat.zip from s3://.../${S3_PREFIX}/qchat.zip" + +# Try download, if hash is invalid we fail. +AWS_ACCESS_KEY_ID="$QCHAT_ACCESSKEY" AWS_SECRET_ACCESS_KEY="$Q_SECRET_ACCESS_KEY" AWS_SESSION_TOKEN="$Q_SESSION_TOKEN" \ + aws s3 cp s3://${CHAT_BUILD_BUCKET_NAME}/${S3_PREFIX}/qchat.zip ./qchat.zip --region us-east-1 + +# Handle the zip file, copy the qchat executable to /usr/local/bin + symlink from old code +echo "Extracting qchat.zip..." +unzip -q qchat.zip + +# move it to /usr/local/bin/qchat for path as qchat may not work otherwise +if cp qchat /usr/local/bin/ && chmod +x /usr/local/bin/qchat; then + ln -sf /usr/local/bin/qchat /usr/local/bin/q + echo "qchat installed successfully" +else + echo "ERROR: Failed to install qchat" + exit 1 +fi + +echo "Cleaning q zip" +rm -f qchat.zip +rm -rf qchat diff --git a/docs-generation/update-docs.sh b/docs-generation/update-docs.sh new file mode 100755 index 0000000000..8f1c29d591 --- /dev/null +++ b/docs-generation/update-docs.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e + +if [ ! -f "$PR_FILE" ]; then + echo "PR file not found, aborting" + exit 1 +fi + +PROMPT="Before making any changes, read the 'docs' directory for the project's current +documentation. Then read 'pr-contents.txt' to see the contents of the current PR.\n\n +After reading both the directory and the PR file, update the files in the 'docs' directory +with new documentation reflecting the proposed changes in the PR. Make new files as appropriate." + +timeout 10m echo -e $PROMPT | qchat chat --non-interactive --trust-all-tools +exit $? \ No newline at end of file diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 0fc45bba3d..7c827ab8ec 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -4,5 +4,7 @@ - [The Agent Format](./agent-format.md) - [Built-in Tools](./built-in-tools.md) +- [Slash Commands Reference](./slash-commands.md) +- [To-Do List Management](./todo-lists.md) - [Knowledge Management](./knowledge-management.md) - [Profile to Agent Migration](./legacy-profile-to-agent-migration.md) diff --git a/docs/agent-file-locations.md b/docs/agent-file-locations.md index c09ed9311b..1da57c5e85 100644 --- a/docs/agent-file-locations.md +++ b/docs/agent-file-locations.md @@ -108,3 +108,23 @@ EOF ## Directory Creation Q CLI will automatically create the global agents directory (`~/.aws/amazonq/cli-agents/`) if it doesn't exist. However, you need to manually create the local agents directory (`.amazonq/cli-agents/`) in your workspace if you want to use local agents. + +## Related Local Storage + +In addition to agent configurations, Q CLI stores other workspace-specific data in the `.amazonq/` directory: + +### To-Do Lists +To-do lists created by the `todo_list` tool are stored in: +``` +.amazonq/cli-todo-lists/ +``` + +These files persist across chat sessions and allow you to resume work on incomplete tasks using the `/todos` slash commands. + +### Legacy Configuration +Legacy MCP server configurations may be stored in: +``` +.amazonq/mcp.json +``` + +This file is used when agents have `useLegacyMcpJson` set to `true`. diff --git a/docs/built-in-tools.md b/docs/built-in-tools.md index 49aaf5d715..84dcc5ddf0 100644 --- a/docs/built-in-tools.md +++ b/docs/built-in-tools.md @@ -8,6 +8,7 @@ Amazon Q CLI includes several built-in tools that agents can use. This document - [`report_issue`](#report_issue-tool) — Open a GitHub issue template. - [`knowledge`](#knowledge-tool) — Store and retrieve information in a knowledge base. - [`thinking`](#thinking-tool) — Internal reasoning mechanism. +- [`todo_list`](#todo_list-tool) — Create and manage to-do lists for multi-step tasks. - [`use_aws`](#use_aws-tool) — Make AWS CLI API calls. ## Execute_bash Tool @@ -102,6 +103,71 @@ An internal reasoning mechanism that improves the quality of complex tasks by br This tool has no configuration options. +## Todo_list Tool + +Create and manage to-do lists for multi-step tasks. This tool helps track progress on complex tasks by breaking them down into manageable steps and marking completion as work progresses. + +The tool automatically creates to-do lists when you give Amazon Q multi-step tasks and tracks completion status. To-do lists are stored locally in the `.amazonq/cli-todo-lists/` directory and persist across chat sessions. + +### Commands + +#### `create` +Creates a new to-do list with specified tasks and description. + +**Required parameters:** +- `tasks`: Array of distinct task descriptions +- `todo_list_description`: Brief summary of the to-do list + +#### `complete` +Marks specified tasks as completed and updates context. + +**Required parameters:** +- `completed_indices`: Array of 0-indexed task numbers to mark complete +- `context_update`: Important information about completed tasks +- `current_id`: ID of the currently loaded to-do list + +**Optional parameters:** +- `modified_files`: Array of file paths that were modified during the task + +#### `load` +Loads an existing to-do list by ID. + +**Required parameters:** +- `load_id`: ID of the to-do list to load + +#### `add` +Adds new tasks to the current to-do list. + +**Required parameters:** +- `new_tasks`: Array of new task descriptions +- `insert_indices`: Array of 0-indexed positions where tasks should be inserted +- `current_id`: ID of the currently loaded to-do list + +**Optional parameters:** +- `new_description`: Updated description if tasks significantly change the goal + +#### `remove` +Removes tasks from the current to-do list. + +**Required parameters:** +- `remove_indices`: Array of 0-indexed positions of tasks to remove +- `current_id`: ID of the currently loaded to-do list + +**Optional parameters:** +- `new_description`: Updated description if removal significantly changes the goal + +### Configuration + +This tool has no configuration options and is trusted by default. + +### Usage Notes + +- To-do lists are automatically created when you give Amazon Q complex, multi-step tasks +- Tasks should be marked as completed immediately after finishing each step +- The tool tracks file modifications and important context for each completed task +- To-do lists persist across chat sessions and can be resumed later +- Use the `/todos` slash command to view, manage, and resume existing to-do lists + ## Use_aws Tool Make AWS CLI API calls with the specified service, operation, and parameters. diff --git a/docs/default-agent-behavior.md b/docs/default-agent-behavior.md index 0510906a60..9a08a5b17c 100644 --- a/docs/default-agent-behavior.md +++ b/docs/default-agent-behavior.md @@ -49,10 +49,11 @@ If no agent is specified or found, Q CLI uses a built-in default agent with the The built-in default agent provides: ### Available Tools -- **All tools**: Uses `"*"` wildcard to include all built-in tools and MCP server tools +- **All tools**: Uses `"*"` wildcard to include all built-in tools (including `todo_list`) and MCP server tools ### Trusted Tools - **fs_read only**: Only the `fs_read` tool is pre-approved and won't prompt for permission +- **todo_list**: The `todo_list` tool is also trusted by default for task management - All other tools will require user confirmation before execution ### Default Resources diff --git a/docs/slash-commands.md b/docs/slash-commands.md new file mode 100644 index 0000000000..ea9d4d183b --- /dev/null +++ b/docs/slash-commands.md @@ -0,0 +1,234 @@ +# Slash Commands Reference + +Amazon Q CLI provides several slash commands that allow you to perform specific actions and manage various features directly within your chat session. Slash commands start with `/` and provide quick access to functionality without needing to exit the chat interface. + +## Available Commands + +### General Commands + +#### `/help` +Display help information about available commands and features. + +#### `/quit` or `/exit` +Exit the current chat session and return to the command line. + +#### `/clear` +Clear the current conversation history while maintaining the same agent and configuration. + +### Agent Management + +#### `/agent list` +List all available agents in your current workspace and global directories. + +#### `/agent create` +Create a new agent configuration. Opens an interactive wizard to set up agent properties. + +#### `/agent switch ` +Switch to a different agent for the current session. + +### Model Management + +#### `/model list` +List all available language models that can be used with Amazon Q. + +#### `/model switch ` +Switch to a different language model for the current session. + +### Conversation Management + +#### `/save ` +Save the current conversation with a given name for later retrieval. + +#### `/load ` +Load a previously saved conversation by name. + +### Subscription Management + +#### `/subscribe` +Display your current Amazon Q subscription status and usage information. + +### To-Do List Management + +The `/todos` command provides comprehensive to-do list management functionality: + +#### `/todos view` +View an existing to-do list. Opens an interactive selection menu to choose from available lists. + +**Usage:** +```bash +/todos view +``` + +**Features:** +- Lists all available to-do lists with completion status +- Shows progress indicators (e.g., "3/5 tasks completed") +- Displays completed lists with ✓ and in-progress lists with ✗ +- Interactive fuzzy search for easy selection + +#### `/todos resume` +Resume working on a selected to-do list. Amazon Q will load the list and continue from where it left off. + +**Usage:** +```bash +/todos resume +``` + +**Features:** +- Automatically loads the selected to-do list state +- Restores previous context and file modifications +- Continues execution from the last completed task +- Provides seamless continuation of interrupted work + +#### `/todos delete` +Delete a specific to-do list or all lists. + +**Usage:** +```bash +/todos delete # Delete a selected list (interactive) +/todos delete --all # Delete all lists (requires confirmation) +``` + +**Options:** +- `--all`: Delete all to-do lists without individual selection + +#### `/todos clear-finished` +Remove all completed to-do lists to clean up your workspace. + +**Usage:** +```bash +/todos clear-finished +``` + +**Features:** +- Only removes lists where all tasks are marked complete +- Preserves in-progress lists +- Provides confirmation of cleanup actions + +### Knowledge Management + +The `/knowledge` command provides persistent knowledge base functionality: + +#### `/knowledge show` +Display all entries in your knowledge base with detailed information. + +#### `/knowledge add [options]` +Add files or directories to your knowledge base. + +**Usage:** +```bash +/knowledge add "project-docs" /path/to/documentation +/knowledge add "rust-code" /path/to/project --include "*.rs" --exclude "target/**" +``` + +**Options:** +- `--include `: Include files matching the pattern +- `--exclude `: Exclude files matching the pattern +- `--index-type `: Choose indexing approach + +#### `/knowledge remove ` +Remove entries from your knowledge base by name, path, or ID. + +#### `/knowledge update ` +Update an existing knowledge base entry with new content. + +#### `/knowledge clear` +Remove all entries from your knowledge base (requires confirmation). + +#### `/knowledge status` +View the status of background indexing operations. + +#### `/knowledge cancel [operation_id]` +Cancel background operations by ID, or all operations if no ID provided. + +## Command Categories + +### Interactive Commands +Commands that open selection menus or wizards: +- `/todos view` +- `/todos resume` +- `/todos delete` (without --all) +- `/agent list` +- `/model list` + +### Immediate Action Commands +Commands that perform actions directly: +- `/todos clear-finished` +- `/todos delete --all` +- `/knowledge show` +- `/clear` +- `/quit` + +### Commands with Arguments +Commands that require additional parameters: +- `/knowledge add ` +- `/save ` +- `/load ` +- `/agent switch ` + +## Tips and Best Practices + +### General Usage +- Use tab completion to discover available commands +- Most commands provide help when used incorrectly +- Commands are case-insensitive +- Use quotes around names or paths with spaces + +### To-Do List Management +- Let Amazon Q create to-do lists automatically for complex tasks +- Use `/todos resume` to continue interrupted work sessions +- Regularly use `/todos clear-finished` to maintain a clean workspace +- View lists with `/todos view` to check progress without resuming + +### Knowledge Management +- Use descriptive names when adding knowledge bases +- Leverage include/exclude patterns to focus on relevant files +- Monitor indexing progress with `/knowledge status` +- Use `/knowledge clear` sparingly as it removes all data + +### Workflow Integration +- Save important conversations before switching agents or models +- Use `/subscribe` to monitor your usage and subscription status +- Combine slash commands with natural language requests for efficient workflows + +## Error Handling + +### Common Error Messages + +**"No to-do lists found"** +- No to-do lists exist in the current directory +- Create complex tasks to generate new lists automatically + +**"Agent not found"** +- The specified agent doesn't exist in current workspace or global directories +- Use `/agent list` to see available agents + +**"Knowledge base operation failed"** +- Check file permissions and disk space +- Verify paths exist and are accessible +- Use `/knowledge status` to check for ongoing operations + +### Recovery Actions +- Most commands can be safely retried after fixing underlying issues +- Use `/clear` to reset conversation state if commands behave unexpectedly +- Check the Amazon Q CLI logs for detailed error information + +## Advanced Usage + +### Combining Commands +You can use multiple slash commands in sequence: +```bash +/knowledge add "current-project" . +/todos resume +/save "project-work-session" +``` + +### Automation Integration +Slash commands work well with: +- Shell scripts that launch Q CLI with specific agents +- Workflow automation that saves/loads conversations +- CI/CD pipelines that use knowledge bases for context + +### Customization +- Configure default knowledge base patterns with `q settings` +- Set default agents to avoid repeated `/agent switch` commands +- Use agent configurations to pre-configure tool permissions for smoother workflows diff --git a/docs/todo-lists.md b/docs/todo-lists.md new file mode 100644 index 0000000000..96d40ffa48 --- /dev/null +++ b/docs/todo-lists.md @@ -0,0 +1,181 @@ +# To-Do List Management + +Amazon Q CLI includes comprehensive to-do list functionality that helps you track progress on complex, multi-step tasks. The system automatically creates to-do lists when you give Amazon Q tasks that require multiple steps, and provides tools to manage and resume these lists across chat sessions. + +## Overview + +The to-do list system consists of two main components: + +1. **`todo_list` tool**: Used by Amazon Q to automatically create and manage to-do lists during task execution +2. **`/todos` slash command**: Allows you to manually view, manage, and resume existing to-do lists + +## How It Works + +### Automatic To-Do List Creation + +When you give Amazon Q a complex task that requires multiple steps, it will automatically: + +1. Create a to-do list with all necessary steps +2. Display the list to show what work will be done +3. Mark off tasks as they are completed +4. Track important context and file modifications +5. Save progress that persists across chat sessions + +### Example Workflow + +``` +User: "Help me set up a new React project with TypeScript, ESLint, and deploy it to AWS" + +Q creates a to-do list: +TODO: + ☐ Initialize new React project with TypeScript + ☐ Configure ESLint with TypeScript rules + ☐ Set up build configuration + ☐ Create AWS deployment configuration + ☐ Deploy to AWS and verify + +Q then works through each task, marking them complete: + ■ Initialize new React project with TypeScript + ☐ Configure ESLint with TypeScript rules + ... +``` + +## Managing To-Do Lists + +### The `/todos` Slash Command + +Use `/todos` followed by a subcommand to manage your to-do lists: + +#### `/todos view` +View an existing to-do list. Opens a selection menu to choose from available lists. + +```bash +/todos view +``` + +#### `/todos resume` +Resume working on a selected to-do list. Amazon Q will load the list and continue from where it left off. + +```bash +/todos resume +``` + +#### `/todos delete` +Delete a specific to-do list or all lists. + +```bash +/todos delete # Delete a selected list +/todos delete --all # Delete all lists +``` + +#### `/todos clear-finished` +Remove all completed to-do lists to clean up your workspace. + +```bash +/todos clear-finished +``` + +## To-Do List Storage + +### Local Storage +To-do lists are stored locally in your current working directory under: +``` +.amazonq/cli-todo-lists/ +``` + +Each to-do list is saved as a JSON file with a unique timestamp-based ID. + +### Persistence +- To-do lists persist across chat sessions +- You can resume work on any incomplete list +- Lists are automatically saved when tasks are completed or modified +- Context and file modifications are tracked for each completed task + +## To-Do List States + +### Task Status +- **☐** Incomplete task (displayed in normal text) +- **■** Completed task (displayed in green italic text) + +### List Status +- **In Progress**: Has incomplete tasks (displayed with ✗ and progress count) +- **Completed**: All tasks finished (displayed with ✓) + +### Display Format +``` +✗ Set up React project (2/5) # In progress +✓ Deploy website # Completed +``` + +## Best Practices + +### For Users +1. **Let Amazon Q create lists**: Don't manually create to-do lists - let Amazon Q generate them based on your requests +2. **Use descriptive requests**: Provide clear, detailed descriptions of what you want to accomplish +3. **Resume incomplete work**: Use `/todos resume` to continue work on unfinished tasks +4. **Clean up regularly**: Use `/todos clear-finished` to remove completed lists + +### For Complex Tasks +1. **Break down large requests**: If you have a very complex project, consider breaking it into smaller, focused requests +2. **Provide context**: Give Amazon Q relevant information about your project, preferences, and constraints +3. **Review progress**: Use `/todos view` to check the status of ongoing work + +## Integration with Chat Sessions + +### Conversation Summaries +When you save a conversation that includes to-do list work, the summary will include: +- The ID of any currently loaded to-do list +- Progress made on tasks +- Important context and insights + +### Resuming Work +When you resume a conversation or load a to-do list: +- Amazon Q automatically loads the list state +- Previous context and file modifications are available +- Work continues from the last completed task + +## Troubleshooting + +### Common Issues + +**To-do lists not appearing** +- Ensure you're in the same directory where the lists were created +- Check that `.amazonq/cli-todo-lists/` exists in your current directory + +**Cannot resume a list** +- Verify the list still exists with `/todos view` +- Check that the list file hasn't been corrupted or manually modified + +**Lists not saving progress** +- Ensure you have write permissions in the current directory +- Check that there's sufficient disk space + +### Error Messages + +**"No to-do lists to [action]"** +- No lists exist in the current directory +- Create a new task that requires multiple steps to generate a list + +**"Could not load to-do list"** +- The list file may be corrupted or manually modified +- Try deleting the problematic list and creating a new one + +## Technical Details + +### File Format +To-do lists are stored as JSON files containing: +- Task descriptions and completion status +- List description and metadata +- Context updates from completed tasks +- Modified file paths +- Unique identifier + +### Security +- Lists are stored locally and never transmitted +- No sensitive information is automatically captured +- File paths and context are only what you explicitly work with + +### Performance +- Lists are loaded on-demand +- Minimal impact on chat session performance +- Automatic cleanup of temporary files diff --git a/pr-contents.txt b/pr-contents.txt new file mode 100644 index 0000000000..624c817f16 --- /dev/null +++ b/pr-contents.txt @@ -0,0 +1,1294 @@ +====== PR Information ======\n +Title: Add to-do list functionality to QCLI +Description: Adds a to-do list tool (called todo_list) with several commands that allow Q to create a to-do list and update it as it completes tasks, along with a slash command (/todos) that allows users to view and manage their in-progress to-do lists. + + +By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. + + +====== PR Diffs ====== + +diff --git a/Cargo.lock b/Cargo.lock +index 5637b427f8..7ec0858992 100644 +--- a/Cargo.lock ++++ b/Cargo.lock +@@ -231,9 +231,9 @@ dependencies = [ + + [[package]] + name = "anyhow" +-version = "1.0.98" ++version = "1.0.99" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" ++checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + + [[package]] + name = "arbitrary" +@@ -670,7 +670,7 @@ dependencies = [ + "regex-lite", + "roxmltree", + "serde_json", +- "thiserror 2.0.12", ++ "thiserror 2.0.14", + ] + + [[package]] +@@ -1052,7 +1052,7 @@ dependencies = [ + "cached_proc_macro_types", + "hashbrown 0.15.5", + "once_cell", +- "thiserror 2.0.12", ++ "thiserror 2.0.14", + "web-time", + ] + +@@ -1305,7 +1305,7 @@ dependencies = [ + "syntect", + "sysinfo", + "tempfile", +- "thiserror 2.0.12", ++ "thiserror 2.0.14", + "time", + "tokio", + "tokio-tungstenite", +@@ -1392,9 +1392,9 @@ dependencies = [ + + [[package]] + name = "clap" +-version = "4.5.43" ++version = "4.5.44" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f" ++checksum = "1c1f056bae57e3e54c3375c41ff79619ddd13460a17d7438712bd0d83fda4ff8" + dependencies = [ + "clap_builder", + "clap_derive", +@@ -1402,9 +1402,9 @@ dependencies = [ + + [[package]] + name = "clap_builder" +-version = "4.5.43" ++version = "4.5.44" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65" ++checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" + dependencies = [ + "anstream", + "anstyle", +@@ -1417,9 +1417,9 @@ dependencies = [ + + [[package]] + name = "clap_complete" +-version = "4.5.56" ++version = "4.5.57" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "67e4efcbb5da11a92e8a609233aa1e8a7d91e38de0be865f016d14700d45a7fd" ++checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad" + dependencies = [ + "clap", + ] +@@ -2762,9 +2762,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + + [[package]] + name = "glob" +-version = "0.3.2" ++version = "0.3.3" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" ++checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + + [[package]] + name = "globset" +@@ -3475,9 +3475,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + + [[package]] + name = "libc" +-version = "0.2.174" ++version = "0.2.175" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" ++checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + + [[package]] + name = "libloading" +@@ -4016,7 +4016,7 @@ dependencies = [ + "serde_json", + "strum 0.26.3", + "strum_macros 0.26.4", +- "thiserror 2.0.12", ++ "thiserror 2.0.14", + "typetag", + "web-time", + "windows-sys 0.48.0", +@@ -4692,9 +4692,9 @@ dependencies = [ + + [[package]] + name = "proc-macro2" +-version = "1.0.96" ++version = "1.0.97" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0" ++checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" + dependencies = [ + "unicode-ident", + ] +@@ -4825,7 +4825,7 @@ dependencies = [ + "rustc-hash 2.1.1", + "rustls 0.23.31", + "socket2 0.5.10", +- "thiserror 2.0.12", ++ "thiserror 2.0.14", + "tokio", + "tracing", + "web-time", +@@ -4846,7 +4846,7 @@ dependencies = [ + "rustls 0.23.31", + "rustls-pki-types", + "slab", +- "thiserror 2.0.12", ++ "thiserror 2.0.14", + "tinyvec", + "tracing", + "web-time", +@@ -5065,7 +5065,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" + dependencies = [ + "getrandom 0.2.16", + "libredox", +- "thiserror 2.0.12", ++ "thiserror 2.0.14", + ] + + [[package]] +@@ -5583,7 +5583,7 @@ dependencies = [ + "serde_json", + "sha2", + "tempfile", +- "thiserror 2.0.12", ++ "thiserror 2.0.14", + "tokenizers", + "tokio", + "tokio-stream", +@@ -6164,12 +6164,12 @@ dependencies = [ + + [[package]] + name = "terminal_size" +-version = "0.4.2" ++version = "0.4.3" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" ++checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" + dependencies = [ + "rustix 1.0.8", +- "windows-sys 0.59.0", ++ "windows-sys 0.60.2", + ] + + [[package]] +@@ -6199,11 +6199,11 @@ dependencies = [ + + [[package]] + name = "thiserror" +-version = "2.0.12" ++version = "2.0.14" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" ++checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" + dependencies = [ +- "thiserror-impl 2.0.12", ++ "thiserror-impl 2.0.14", + ] + + [[package]] +@@ -6219,9 +6219,9 @@ dependencies = [ + + [[package]] + name = "thiserror-impl" +-version = "2.0.12" ++version = "2.0.14" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" ++checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" + dependencies = [ + "proc-macro2", + "quote", +@@ -6342,7 +6342,7 @@ dependencies = [ + "serde", + "serde_json", + "spm_precompiled", +- "thiserror 2.0.12", ++ "thiserror 2.0.14", + "unicode-normalization-alignments", + "unicode-segmentation", + "unicode_categories", +@@ -6687,7 +6687,7 @@ dependencies = [ + "log", + "rand 0.9.2", + "sha1", +- "thiserror 2.0.12", ++ "thiserror 2.0.14", + "utf-8", + ] + +@@ -6848,9 +6848,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + + [[package]] + name = "uuid" +-version = "1.17.0" ++version = "1.18.0" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" ++checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" + dependencies = [ + "getrandom 0.3.3", + "js-sys", +@@ -7725,7 +7725,7 @@ dependencies = [ + "os_pipe", + "rustix 0.38.44", + "tempfile", +- "thiserror 2.0.12", ++ "thiserror 2.0.14", + "tree_magic_mini", + "wayland-backend", + "wayland-client", +diff --git a/build-config/buildspec-linux.yml b/build-config/buildspec-linux.yml +index fb50d66f5b..8c20235acf 100644 +--- a/build-config/buildspec-linux.yml ++++ b/build-config/buildspec-linux.yml +@@ -44,4 +44,3 @@ artifacts: + # Signatures + - ./*.asc + - ./*.sig +- +diff --git a/build-config/buildspec-macos.yml b/build-config/buildspec-macos.yml +index 0fbe335ae2..bb9bb58d75 100644 +--- a/build-config/buildspec-macos.yml ++++ b/build-config/buildspec-macos.yml +@@ -38,4 +38,3 @@ artifacts: + - ./*.zip + # Hashes + - ./*.sha256 +- +diff --git a/crates/chat-cli/src/cli/agent/mod.rs b/crates/chat-cli/src/cli/agent/mod.rs +index a11c0cb7e2..f2ff3954aa 100644 +--- a/crates/chat-cli/src/cli/agent/mod.rs ++++ b/crates/chat-cli/src/cli/agent/mod.rs +@@ -728,6 +728,7 @@ impl Agents { + "use_aws" => "trust read-only commands".dark_grey(), + "report_issue" => "trusted".dark_green().bold(), + "thinking" => "trusted (prerelease)".dark_green().bold(), ++ "todo_list" => "trusted".dark_green().bold(), + _ if self.trust_all_tools => "trusted".dark_grey().bold(), + _ => "not trusted".dark_grey(), + }; +diff --git a/crates/chat-cli/src/cli/chat/cli/mod.rs b/crates/chat-cli/src/cli/chat/cli/mod.rs +index 4805426d06..12bd0aa51b 100644 +--- a/crates/chat-cli/src/cli/chat/cli/mod.rs ++++ b/crates/chat-cli/src/cli/chat/cli/mod.rs +@@ -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; + +@@ -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; +@@ -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 { +@@ -146,6 +151,7 @@ impl SlashCommand { + // skip_printing_tools: true, + // }) + // }, ++ Self::Todos(subcommand) => subcommand.execute(os, session).await, + } + } + +@@ -171,6 +177,7 @@ impl SlashCommand { + PersistSubcommand::Save { .. } => "save", + PersistSubcommand::Load { .. } => "load", + }, ++ Self::Todos(_) => "todos", + } + } + +diff --git a/crates/chat-cli/src/cli/chat/cli/todos.rs b/crates/chat-cli/src/cli/chat/cli/todos.rs +new file mode 100644 +index 0000000000..eb107e8110 +--- /dev/null ++++ b/crates/chat-cli/src/cli/chat/cli/todos.rs +@@ -0,0 +1,202 @@ ++use clap::Subcommand; ++use crossterm::execute; ++use crossterm::style::{ ++ self, ++ Stylize, ++}; ++use dialoguer::FuzzySelect; ++use eyre::Result; ++ ++use crate::cli::chat::tools::todo::{ ++ TodoListState, ++ delete_todo, ++ get_all_todos, ++}; ++use crate::cli::chat::{ ++ ChatError, ++ ChatSession, ++ ChatState, ++}; ++use crate::os::Os; ++ ++/// Defines subcommands that allow users to view and manage todo lists ++#[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 { ++ match self { ++ Self::ClearFinished => { ++ let (todos, errors) = match 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 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 = TodoListState::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 { ++ 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() { ++ 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> { ++ let mut out = Vec::new(); ++ let (todos, _) = 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 { ++ FuzzySelect::new() ++ .with_prompt(prompt_str) ++ .items(entries) ++ .report(false) ++ .interact_opt() ++ .unwrap_or(None) ++} +diff --git a/crates/chat-cli/src/cli/chat/conversation.rs b/crates/chat-cli/src/cli/chat/conversation.rs +index ca7b87d2c4..8718696529 100644 +--- a/crates/chat-cli/src/cli/chat/conversation.rs ++++ b/crates/chat-cli/src/cli/chat/conversation.rs +@@ -12,6 +12,7 @@ use crossterm::{ + execute, + style, + }; ++use eyre::Result; + use serde::{ + Deserialize, + Serialize, +@@ -489,12 +490,15 @@ impl ConversationState { + 2) Bullet points for all significant tools executed and their results\n\ + 3) Bullet points for any code or technical information shared\n\ + 4) A section of key insights gained\n\n\ ++ 5) REQUIRED: the ID of the currently loaded todo list, if any\n\n\ + FORMAT THE SUMMARY IN THIRD PERSON, NOT AS A DIRECT RESPONSE. Example format:\n\n\ + ## CONVERSATION SUMMARY\n\ + * Topic 1: Key information\n\ + * Topic 2: Key information\n\n\ + ## TOOLS EXECUTED\n\ + * Tool X: Result Y\n\n\ ++ ## TODO ID\n\ ++ * \n\n\ + Remember this is a DOCUMENT not a chat response. The custom instruction above modifies what to prioritize.\n\ + FILTER OUT CHAT CONVENTIONS (greetings, offers to help, etc).", + custom_prompt.as_ref() +@@ -509,12 +513,15 @@ impl ConversationState { + 2) Bullet points for all significant tools executed and their results\n\ + 3) Bullet points for any code or technical information shared\n\ + 4) A section of key insights gained\n\n\ ++ 5) REQUIRED: the ID of the currently loaded todo list, if any\n\n\ + FORMAT THE SUMMARY IN THIRD PERSON, NOT AS A DIRECT RESPONSE. Example format:\n\n\ + ## CONVERSATION SUMMARY\n\ + * Topic 1: Key information\n\ + * Topic 2: Key information\n\n\ + ## TOOLS EXECUTED\n\ + * Tool X: Result Y\n\n\ ++ ## TODO ID\n\ ++ * \n\n\ + Remember this is a DOCUMENT not a chat response.\n\ + FILTER OUT CHAT CONVENTIONS (greetings, offers to help, etc).".to_string() + }, +diff --git a/crates/chat-cli/src/cli/chat/mod.rs b/crates/chat-cli/src/cli/chat/mod.rs +index 0d3b7b1d8c..e159d66641 100644 +--- a/crates/chat-cli/src/cli/chat/mod.rs ++++ b/crates/chat-cli/src/cli/chat/mod.rs +@@ -131,6 +131,7 @@ use crate::api_client::{ + }; + use crate::auth::AuthError; + use crate::auth::builder_id::is_idc_user; ++use crate::cli::TodoListState; + use crate::cli::agent::Agents; + use crate::cli::chat::cli::SlashCommand; + use crate::cli::chat::cli::model::find_model; +@@ -138,6 +139,8 @@ 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; +@@ -639,6 +642,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 _ = TodoListState::init_dir(os).await; ++ + Ok(Self { + stdout, + stderr, +@@ -2778,6 +2786,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 { ++ // Have to unpack each value separately since Reports can't be converted to ++ // ChatError ++ let todo_list = match TodoListState::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(); ++ 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. +@@ -2838,7 +2893,7 @@ async fn get_subscription_status_with_spinner( + .await; + } + +-async fn with_spinner(output: &mut impl std::io::Write, spinner_text: &str, f: F) -> Result ++pub async fn with_spinner(output: &mut impl std::io::Write, spinner_text: &str, f: F) -> Result + where + F: FnOnce() -> Fut, + Fut: std::future::Future>, +diff --git a/crates/chat-cli/src/cli/chat/prompt.rs b/crates/chat-cli/src/cli/chat/prompt.rs +index 291fe35ba3..fc0aef58da 100644 +--- a/crates/chat-cli/src/cli/chat/prompt.rs ++++ b/crates/chat-cli/src/cli/chat/prompt.rs +@@ -83,6 +83,11 @@ pub const COMMANDS: &[&str] = &[ + "/save", + "/load", + "/subscribe", ++ "/todos", ++ "/todos resume", ++ "/todos clear-finished", ++ "/todos view", ++ "/todos delete", + ]; + + /// Complete commands that start with a slash +diff --git a/crates/chat-cli/src/cli/chat/tool_manager.rs b/crates/chat-cli/src/cli/chat/tool_manager.rs +index 95739a1068..f23aa6910d 100644 +--- a/crates/chat-cli/src/cli/chat/tool_manager.rs ++++ b/crates/chat-cli/src/cli/chat/tool_manager.rs +@@ -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::TodoList; + use crate::cli::chat::tools::use_aws::UseAws; + use crate::cli::chat::tools::{ + Tool, +@@ -1066,6 +1067,7 @@ impl ToolManager { + "report_issue" => Tool::GhIssue(serde_json::from_value::(value.args).map_err(map_err)?), + "thinking" => Tool::Thinking(serde_json::from_value::(value.args).map_err(map_err)?), + "knowledge" => Tool::Knowledge(serde_json::from_value::(value.args).map_err(map_err)?), ++ "todo_list" => Tool::Todo(serde_json::from_value::(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 +diff --git a/crates/chat-cli/src/cli/chat/tools/mod.rs b/crates/chat-cli/src/cli/chat/tools/mod.rs +index ea2aef2529..fef93b62a6 100644 +--- a/crates/chat-cli/src/cli/chat/tools/mod.rs ++++ b/crates/chat-cli/src/cli/chat/tools/mod.rs +@@ -5,6 +5,7 @@ pub mod fs_write; + pub mod gh_issue; + pub mod knowledge; + pub mod thinking; ++pub mod todo; + pub mod use_aws; + + use std::borrow::{ +@@ -35,6 +36,7 @@ use serde::{ + Serialize, + }; + use thinking::Thinking; ++use todo::TodoList; + use tracing::error; + use use_aws::UseAws; + +@@ -79,6 +81,7 @@ pub enum Tool { + GhIssue(GhIssue), + Knowledge(Knowledge), + Thinking(Thinking), ++ Todo(TodoList), + } + + impl Tool { +@@ -96,6 +99,7 @@ impl Tool { + Tool::GhIssue(_) => "gh_issue", + Tool::Knowledge(_) => "knowledge", + Tool::Thinking(_) => "thinking (prerelease)", ++ Tool::Todo(_) => "todo_list", + } + .to_owned() + } +@@ -111,6 +115,7 @@ impl Tool { + Tool::GhIssue(_) => PermissionEvalResult::Allow, + Tool::Thinking(_) => PermissionEvalResult::Allow, + Tool::Knowledge(knowledge) => knowledge.eval_perm(agent), ++ Tool::Todo(_) => PermissionEvalResult::Allow, + } + } + +@@ -130,6 +135,7 @@ impl Tool { + Tool::GhIssue(gh_issue) => gh_issue.invoke(os, stdout).await, + Tool::Knowledge(knowledge) => knowledge.invoke(os, stdout).await, + Tool::Thinking(think) => think.invoke(stdout).await, ++ Tool::Todo(todo) => todo.invoke(os, stdout).await, + } + } + +@@ -144,6 +150,7 @@ impl Tool { + Tool::GhIssue(gh_issue) => gh_issue.queue_description(output), + Tool::Knowledge(knowledge) => knowledge.queue_description(os, output).await, + Tool::Thinking(thinking) => thinking.queue_description(output), ++ Tool::Todo(_) => Ok(()), + } + } + +@@ -158,6 +165,7 @@ impl Tool { + Tool::GhIssue(gh_issue) => gh_issue.validate(os).await, + Tool::Knowledge(knowledge) => knowledge.validate(os).await, + Tool::Thinking(think) => think.validate(os).await, ++ Tool::Todo(todo) => todo.validate(os).await, + } + } + +diff --git a/crates/chat-cli/src/cli/chat/tools/todo.rs b/crates/chat-cli/src/cli/chat/tools/todo.rs +new file mode 100644 +index 0000000000..f874e962d0 +--- /dev/null ++++ b/crates/chat-cli/src/cli/chat/tools/todo.rs +@@ -0,0 +1,402 @@ ++use std::collections::HashSet; ++use std::io::Write; ++use std::path::PathBuf; ++use std::time::{ ++ SystemTime, ++ UNIX_EPOCH, ++}; ++ ++use crossterm::style::Stylize; ++use crossterm::{ ++ queue, ++ style, ++}; ++use eyre::{ ++ OptionExt, ++ Report, ++ Result, ++ bail, ++ eyre, ++}; ++use serde::{ ++ Deserialize, ++ Serialize, ++}; ++ ++use super::InvokeOutput; ++use crate::os::Os; ++ ++// Local directory to store todo lists ++const TODO_LIST_DIR: &str = ".amazonq/cli-todo-lists/"; ++ ++/// Contains all state to be serialized and deserialized into a todo list ++#[derive(Debug, Default, Serialize, Deserialize, Clone)] ++pub struct TodoListState { ++ pub tasks: Vec, ++ pub completed: Vec, ++ pub description: String, ++ pub context: Vec, ++ pub modified_files: Vec, ++ pub id: String, ++} ++ ++impl TodoListState { ++ /// Creates a local directory to store todo lists ++ pub async fn init_dir(os: &Os) -> Result<()> { ++ os.fs.create_dir_all(os.env.current_dir()?.join(TODO_LIST_DIR)).await?; ++ Ok(()) ++ } ++ ++ /// Loads a TodoListState with the given id ++ pub async fn load(os: &Os, id: &str) -> Result { ++ let state_str = os ++ .fs ++ .read_to_string(id_to_path(os, id)?) ++ .await ++ .map_err(|e| eyre!("Could not load todo list: {e}"))?; ++ serde_json::from_str::(&state_str).map_err(|e| eyre!("Could not deserialize todo list: {e}")) ++ } ++ ++ /// Saves this TodoListState with the given id ++ pub async fn save(&self, os: &Os, id: &str) -> Result<()> { ++ let path = id_to_path(os, id)?; ++ Self::init_dir(os).await?; ++ if !os.fs.exists(&path) { ++ os.fs.create_new(&path).await?; ++ } ++ os.fs.write(path, serde_json::to_string(self)?).await?; ++ Ok(()) ++ } ++ ++ /// Displays the TodoListState as a to-do list ++ pub fn display_list(&self, output: &mut impl Write) -> Result<()> { ++ queue!(output, style::Print("TODO:\n".yellow()))?; ++ for (index, (task, completed)) in self.tasks.iter().zip(self.completed.iter()).enumerate() { ++ queue_next_without_newline(output, task.clone(), *completed)?; ++ if index < self.tasks.len() - 1 { ++ queue!(output, style::Print("\n"))?; ++ } ++ } ++ Ok(()) ++ } ++} ++ ++/// Displays a single empty or marked off to-do list task depending on ++/// the completion status ++fn queue_next_without_newline(output: &mut impl Write, task: String, completed: bool) -> Result<()> { ++ if completed { ++ queue!( ++ output, ++ style::SetAttribute(style::Attribute::Italic), ++ style::SetForegroundColor(style::Color::Green), ++ style::Print(" ■ "), ++ style::SetForegroundColor(style::Color::DarkGrey), ++ style::Print(task), ++ style::SetAttribute(style::Attribute::NoItalic), ++ )?; ++ } else { ++ queue!( ++ output, ++ style::SetForegroundColor(style::Color::Reset), ++ style::Print(format!(" ☐ {task}")), ++ )?; ++ } ++ Ok(()) ++} ++ ++/// Generates a new unique id be used for new to-do lists ++pub fn generate_new_todo_id() -> String { ++ let timestamp = SystemTime::now() ++ .duration_since(UNIX_EPOCH) ++ .expect("Time went backwards") ++ .as_millis(); ++ ++ format!("{timestamp}") ++} ++ ++/// Converts a todo list id to an absolute path in the cwd ++pub fn id_to_path(os: &Os, id: &str) -> Result { ++ Ok(os.env.current_dir()?.join(TODO_LIST_DIR).join(format!("{id}.json"))) ++} ++ ++/// Gets all todo lists from the local directory ++pub async fn get_all_todos(os: &Os) -> Result<(Vec, Vec)> { ++ let todo_list_dir = os.env.current_dir()?.join(TODO_LIST_DIR); ++ let mut read_dir_output = os.fs.read_dir(todo_list_dir).await?; ++ ++ let mut todos = Vec::new(); ++ let mut errors = Vec::new(); ++ ++ while let Some(entry) = read_dir_output.next_entry().await? { ++ match TodoListState::load( ++ os, ++ &entry ++ .path() ++ .with_extension("") ++ .file_name() ++ .ok_or_eyre("Path is not a file")? ++ .to_string_lossy(), ++ ) ++ .await ++ { ++ Ok(todo) => todos.push(todo), ++ Err(e) => errors.push(e), ++ }; ++ } ++ ++ Ok((todos, errors)) ++} ++ ++/// Deletes a todo list ++pub async fn delete_todo(os: &Os, id: &str) -> Result<()> { ++ os.fs.remove_file(id_to_path(os, id)?).await?; ++ Ok(()) ++} ++ ++/// Contains the command definitions that allow the model to create, ++/// modify, and mark todo list tasks as complete ++#[derive(Debug, Clone, Deserialize)] ++#[serde(tag = "command", rename_all = "camelCase")] ++pub enum TodoList { ++ // Creates a todo list ++ Create { ++ tasks: Vec, ++ todo_list_description: String, ++ }, ++ ++ // Completes tasks corresponding to the provided indices ++ // on the currently loaded todo list ++ Complete { ++ completed_indices: Vec, ++ context_update: String, ++ modified_files: Option>, ++ current_id: String, ++ }, ++ ++ // Loads a todo list with the given id ++ Load { ++ load_id: String, ++ }, ++ ++ // Inserts new tasks into the current todo list ++ Add { ++ new_tasks: Vec, ++ insert_indices: Vec, ++ new_description: Option, ++ current_id: String, ++ }, ++ ++ // Removes tasks from the current todo list ++ Remove { ++ remove_indices: Vec, ++ new_description: Option, ++ current_id: String, ++ }, ++} ++ ++impl TodoList { ++ pub async fn invoke(&self, os: &Os, output: &mut impl Write) -> Result { ++ let (state, id) = match self { ++ TodoList::Create { ++ tasks, ++ todo_list_description: task_description, ++ } => { ++ let new_id = generate_new_todo_id(); ++ ++ // Create a new todo list with the given tasks and save state ++ let state = TodoListState { ++ tasks: tasks.clone(), ++ completed: vec![false; tasks.len()], ++ description: task_description.clone(), ++ context: Vec::new(), ++ modified_files: Vec::new(), ++ id: new_id.clone(), ++ }; ++ state.save(os, &new_id).await?; ++ state.display_list(output)?; ++ (state, new_id) ++ }, ++ TodoList::Complete { ++ completed_indices, ++ context_update, ++ modified_files, ++ current_id: id, ++ } => { ++ let mut state = TodoListState::load(os, id).await?; ++ ++ for i in completed_indices.iter() { ++ state.completed[*i] = true; ++ } ++ ++ state.context.push(context_update.clone()); ++ ++ if let Some(files) = modified_files { ++ state.modified_files.extend_from_slice(files); ++ } ++ state.save(os, id).await?; ++ ++ // As tasks are being completed, display only the newly completed tasks ++ // and the next. Only display the whole list when all tasks are completed ++ let last_completed = completed_indices.iter().max().unwrap(); ++ if *last_completed == state.tasks.len() - 1 || state.completed.iter().all(|c| *c) { ++ state.display_list(output)?; ++ } else { ++ let mut display_list = TodoListState { ++ tasks: completed_indices.iter().map(|i| state.tasks[*i].clone()).collect(), ++ ..Default::default() ++ }; ++ for _ in 0..completed_indices.len() { ++ display_list.completed.push(true); ++ } ++ ++ // For next state, mark it true/false depending on actual completion state ++ // This only matters when the model skips around tasks ++ display_list.tasks.push(state.tasks[*last_completed + 1].clone()); ++ display_list.completed.push(state.completed[*last_completed + 1]); ++ ++ display_list.display_list(output)?; ++ } ++ (state, id.clone()) ++ }, ++ TodoList::Load { load_id: id } => { ++ let state = TodoListState::load(os, id).await?; ++ state.display_list(output)?; ++ (state, id.clone()) ++ }, ++ TodoList::Add { ++ new_tasks, ++ insert_indices, ++ new_description, ++ current_id: id, ++ } => { ++ let mut state = TodoListState::load(os, id).await?; ++ for (i, task) in insert_indices.iter().zip(new_tasks.iter()) { ++ state.tasks.insert(*i, task.clone()); ++ state.completed.insert(*i, false); ++ } ++ if let Some(description) = new_description { ++ state.description = description.clone(); ++ } ++ state.save(os, id).await?; ++ state.display_list(output)?; ++ (state, id.clone()) ++ }, ++ TodoList::Remove { ++ remove_indices, ++ new_description, ++ current_id: id, ++ } => { ++ let mut state = TodoListState::load(os, id).await?; ++ ++ // Remove entries in reverse order so indices aren't mismatched ++ let mut remove_indices = remove_indices.clone(); ++ remove_indices.sort(); ++ for i in remove_indices.iter().rev() { ++ state.tasks.remove(*i); ++ state.completed.remove(*i); ++ } ++ if let Some(description) = new_description { ++ state.description = description.clone(); ++ } ++ state.save(os, id).await?; ++ state.display_list(output)?; ++ (state, id.clone()) ++ }, ++ }; ++ ++ let invoke_output = format!("TODO LIST STATE: {}\n\n ID: {id}", serde_json::to_string(&state)?); ++ Ok(InvokeOutput { ++ output: super::OutputKind::Text(invoke_output), ++ }) ++ } ++ ++ pub async fn validate(&mut self, os: &Os) -> Result<()> { ++ match self { ++ TodoList::Create { ++ tasks, ++ todo_list_description: task_description, ++ } => { ++ if tasks.is_empty() { ++ bail!("No tasks were provided"); ++ } else if tasks.iter().any(|task| task.trim().is_empty()) { ++ bail!("Tasks cannot be empty"); ++ } else if task_description.is_empty() { ++ bail!("No task description was provided"); ++ } ++ }, ++ TodoList::Complete { ++ completed_indices, ++ context_update, ++ current_id, ++ .. ++ } => { ++ let state = TodoListState::load(os, current_id).await?; ++ if completed_indices.is_empty() { ++ bail!("At least one completed index must be provided"); ++ } else if context_update.is_empty() { ++ bail!("No context update was provided"); ++ } ++ for i in completed_indices.iter() { ++ if *i >= state.tasks.len() { ++ bail!("Index {i} is out of bounds for length {}, ", state.tasks.len()); ++ } ++ } ++ }, ++ TodoList::Load { load_id: id } => { ++ let state = TodoListState::load(os, id).await?; ++ if state.tasks.is_empty() { ++ bail!("Loaded todo list is empty"); ++ } ++ }, ++ TodoList::Add { ++ new_tasks, ++ insert_indices, ++ new_description, ++ current_id: id, ++ } => { ++ let state = TodoListState::load(os, id).await?; ++ if new_tasks.iter().any(|task| task.trim().is_empty()) { ++ bail!("New tasks cannot be empty"); ++ } else if has_duplicates(insert_indices) { ++ bail!("Insertion indices must be unique") ++ } else if new_tasks.len() != insert_indices.len() { ++ bail!("Must provide an index for every new task"); ++ } else if new_description.is_some() && new_description.as_ref().unwrap().trim().is_empty() { ++ bail!("New description cannot be empty"); ++ } ++ for i in insert_indices.iter() { ++ if *i > state.tasks.len() { ++ bail!("Index {i} is out of bounds for length {}, ", state.tasks.len()); ++ } ++ } ++ }, ++ TodoList::Remove { ++ remove_indices, ++ new_description, ++ current_id: id, ++ } => { ++ let state = TodoListState::load(os, id).await?; ++ if has_duplicates(remove_indices) { ++ bail!("Removal indices must be unique") ++ } else if new_description.is_some() && new_description.as_ref().unwrap().trim().is_empty() { ++ bail!("New description cannot be empty"); ++ } ++ for i in remove_indices.iter() { ++ if *i >= state.tasks.len() { ++ bail!("Index {i} is out of bounds for length {}, ", state.tasks.len()); ++ } ++ } ++ }, ++ } ++ Ok(()) ++ } ++} ++ ++/// Generated by Q ++fn has_duplicates(vec: &[T]) -> bool ++where ++ T: std::hash::Hash + Eq, ++{ ++ let mut seen = HashSet::with_capacity(vec.len()); ++ vec.iter().any(|item| !seen.insert(item)) ++} +diff --git a/crates/chat-cli/src/cli/chat/tools/tool_index.json b/crates/chat-cli/src/cli/chat/tools/tool_index.json +index 067e5df1ec..44def78fca 100644 +--- a/crates/chat-cli/src/cli/chat/tools/tool_index.json ++++ b/crates/chat-cli/src/cli/chat/tools/tool_index.json +@@ -281,5 +281,88 @@ + "command" + ] + } ++ }, ++ "todo_list": { ++ "name": "todo_list", ++ "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.", ++ "input_schema": { ++ "type": "object", ++ "properties": { ++ "command": { ++ "type": "string", ++ "enum": [ ++ "create", ++ "complete", ++ "load", ++ "add", ++ "remove" ++ ], ++ "description": "The command to run. Allowed options are `create`, `complete`, `load`, `add`, and `remove`." ++ }, ++ "tasks": { ++ "description": "Required parameter of `create` command containing the list of DISTINCT tasks to be added to the TODO list.", ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "todo_list_description": { ++ "description": "Required parameter of `create` command containing a BRIEF summary of the todo list being created. The summary should be detailed enough to refer to without knowing the problem context beforehand.", ++ "type": "string" ++ }, ++ "completed_indices": { ++ "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.", ++ "type": "array", ++ "items": { ++ "type": "integer" ++ } ++ }, ++ "context_update": { ++ "description": "Required parameter of `complete` command containing important task context. Use this command to track important information about the task AND information about files you have read.", ++ "type": "string" ++ }, ++ "modified_files": { ++ "description": "Optional parameter of `complete` command containing a list of paths of files that were modified during the task. This is useful for tracking file changes that are important to the task.", ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "load_id": { ++ "description": "Required parameter of `load` command containing ID of todo list to load", ++ "type": "string" ++ }, ++ "current_id": { ++ "description": "Required parameter of `complete`, `add`, and `remove` commands containing the ID of the currently loaded todo list. The ID will ALWAYS be provided after every `todo_list` call after the serialized todo list state.", ++ "type": "string" ++ }, ++ "new_tasks": { ++ "description": "Required parameter of `add` command containing a list of new tasks to be added to the to-do list.", ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "insert_indices": { ++ "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.", ++ "type": "array", ++ "items": { ++ "type": "integer" ++ } ++ }, ++ "new_description": { ++ "description": "Optional parameter of `add` and `remove` containing a new todo list description. Use this when the updated set of tasks significantly change the goal or overall procedure of the todo list.", ++ "type": "string" ++ }, ++ "remove_indices": { ++ "description": "Required parameter of `remove` command containing a list of 0-INDEXED positions of tasks to remove.", ++ "type": "array", ++ "items": { ++ "type": "integer" ++ } ++ } ++ }, ++ "required": ["command"] ++ } + } + } +\ No newline at end of file +diff --git a/crates/chat-cli/src/cli/mod.rs b/crates/chat-cli/src/cli/mod.rs +index 33238b9da3..803c35d5c5 100644 +--- a/crates/chat-cli/src/cli/mod.rs ++++ b/crates/chat-cli/src/cli/mod.rs +@@ -18,6 +18,7 @@ use std::process::ExitCode; + use agent::AgentArgs; + use anstream::println; + pub use chat::ConversationState; ++pub use chat::tools::todo::TodoListState; + use clap::{ + ArgAction, + CommandFactory,