Skip to content

Commit 30fc467

Browse files
author
kiran-garre
committed
Slash command + persistence in progress commit
1 parent 415624a commit 30fc467

File tree

9 files changed

+430
-47
lines changed

9 files changed

+430
-47
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod prompts;
1212
pub mod subscribe;
1313
pub mod tools;
1414
pub mod usage;
15+
pub mod todos;
1516

1617
use clap::Parser;
1718
use clear::ClearArgs;
@@ -26,6 +27,7 @@ use persist::PersistSubcommand;
2627
use profile::ProfileSubcommand;
2728
use prompts::PromptsArgs;
2829
use tools::ToolsArgs;
30+
use todos::TodoSubcommand;
2931

3032
use crate::cli::chat::cli::subscribe::SubscribeArgs;
3133
use crate::cli::chat::cli::usage::UsageArgs;
@@ -82,6 +84,8 @@ pub enum SlashCommand {
8284
Persist(PersistSubcommand),
8385
// #[command(flatten)]
8486
// Root(RootSubcommand),
87+
#[command(subcommand)]
88+
Todos(TodoSubcommand),
8589
}
8690

8791
impl SlashCommand {
@@ -120,6 +124,8 @@ impl SlashCommand {
120124
// skip_printing_tools: true,
121125
// })
122126
// },
127+
128+
Self::Todos(subcommand) => subcommand.execute(os, session).await,
123129
}
124130
}
125131
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use clap::Args;
2+
use crate::os::Os;
3+
use crossterm::{
4+
cursor,
5+
execute,
6+
queue,
7+
style,
8+
};
9+
10+
use crate::cli::chat::{
11+
ChatError,
12+
ChatSession,
13+
ChatState,
14+
};
15+
16+
const TODO_INTERNAL_PROMPT: &str = "This is the internal prompt that will be
17+
sent to create the todo list";
18+
19+
#[derive(Debug, Clone, PartialEq, Eq, Default, Args)]
20+
pub struct TodoArgs {
21+
// Task/prompt to generate TODO list for
22+
task_prompt: String,
23+
}
24+
25+
impl TodoArgs {
26+
pub async fn execute(self, os: &Os, session: &mut ChatSession) -> Result<ChatState, ChatError> {
27+
execute!(
28+
session.stderr,
29+
style::Print("Dummy? Who you callin' a dummy?\n")
30+
)?;
31+
Ok(ChatState::PromptUser { skip_printing_tools: true })
32+
}
33+
34+
// pub async fn create_todo_request(os: &Os) {
35+
36+
// }
37+
}
38+
/*
39+
async fn generate_todo(os: &Os, prompt: &str) {
40+
// Create todo request using string above
41+
// This will be a conversation state method
42+
}
43+
44+
*/
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#![allow(warnings)]
2+
3+
use clap::Subcommand;
4+
use std::{io, path::PathBuf};
5+
use crate::{cli::chat::tools::todo::TodoState, os::Os};
6+
use crossterm::{
7+
execute,
8+
style::{self, Stylize},
9+
};
10+
11+
use crate::cli::chat::{
12+
ChatError,
13+
ChatSession,
14+
ChatState,
15+
};
16+
17+
use eyre::{
18+
Result,
19+
bail,
20+
};
21+
22+
use dialoguer::{
23+
FuzzySelect
24+
};
25+
26+
const TODO_INTERNAL_PROMPT: &str = "This is the internal prompt that will be
27+
sent to create the todo list";
28+
29+
// ########### REMOVE BEFORE PUSHING ##############
30+
const TODO_STATE_FOLDER_PATH: &str = "/Users/kiranbug/.aws/amazonq/todos"; // temporary path where we store state files
31+
// ########### --------------------- ##############
32+
33+
34+
#[derive(Debug, PartialEq, Subcommand)]
35+
pub enum TodoSubcommand {
36+
// Task/prompt to generate TODO list for
37+
Show,
38+
ClearFinished,
39+
Select,
40+
}
41+
42+
pub struct TodoDisplayEntry {
43+
pub num_completed: usize,
44+
pub num_tasks: usize,
45+
pub description: String,
46+
pub path: PathBuf,
47+
}
48+
49+
impl std::fmt::Display for TodoDisplayEntry {
50+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51+
if self.num_completed == self.num_tasks {
52+
write!(f, "{} {}",
53+
"✓".green().bold(),
54+
self.description.clone(),
55+
)
56+
} else {
57+
write!(f, "{} {} ({}/{})",
58+
"✗".red().bold(),
59+
self.description.clone(),
60+
self.num_completed,
61+
self.num_tasks
62+
)
63+
}
64+
}
65+
}
66+
67+
impl TodoSubcommand {
68+
pub async fn execute(self, os: &mut Os, session: &mut ChatSession) -> Result<ChatState, ChatError> {
69+
70+
execute!(
71+
session.stderr,
72+
style::Print("Dummy? Who you callin' a dummy?\n")
73+
)?;
74+
75+
match self {
76+
Self::Show => {
77+
match self.get_descriptions_and_statuses(os).await {
78+
Ok(entries) => {
79+
for e in entries {
80+
execute!(
81+
session.stderr,
82+
style::Print(e),
83+
style::Print("\n"),
84+
);
85+
}
86+
}
87+
Err(e) => {
88+
execute!(
89+
session.stderr,
90+
style::Print("Could not show to-do lists"),
91+
);
92+
}
93+
}
94+
},
95+
Self::ClearFinished => {
96+
();
97+
},
98+
Self::Select => {
99+
match self.get_descriptions_and_statuses(os).await {
100+
Ok(entries) => {
101+
let selection_index = FuzzySelect::new()
102+
.items(&entries)
103+
.interact()
104+
.unwrap(); // FIX THIS
105+
return session.resume_todo(os, entries[selection_index].path.clone()).await;
106+
}
107+
Err(e) => println!("{:?}", e),
108+
};
109+
},
110+
};
111+
Ok(ChatState::PromptUser { skip_printing_tools: true })
112+
}
113+
114+
async fn get_descriptions_and_statuses(self, os: &Os) -> Result<Vec<TodoDisplayEntry>> {
115+
let mut out = Vec::new();
116+
let mut entries = os.fs.read_dir(TODO_STATE_FOLDER_PATH).await?;
117+
118+
while let Some(entry) = entries.next_entry().await? {
119+
let contents = os.fs.read_to_string(entry.path()).await?;
120+
let temp_struct = match serde_json::from_str::<TodoState>(&contents) {
121+
Ok(state) => state,
122+
Err(_) => continue,
123+
};
124+
out.push(TodoDisplayEntry {
125+
num_completed: temp_struct.completed.iter().filter(|b| **b).count(),
126+
num_tasks: temp_struct.completed.len(),
127+
description: temp_struct.task_description,
128+
path: entry.path(),
129+
});
130+
}
131+
Ok(out)
132+
}
133+
134+
}

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::collections::{
44
VecDeque,
55
};
66
use std::io::Write;
7+
use std::path::PathBuf;
78
use std::sync::atomic::Ordering;
89

910
use crossterm::style::Color;
@@ -21,6 +22,10 @@ use tracing::{
2122
warn,
2223
};
2324

25+
use eyre::{
26+
Result,
27+
};
28+
2429
use super::consts::{
2530
DUMMY_TOOL_NAME,
2631
MAX_CHARS,
@@ -73,6 +78,8 @@ use crate::cli::chat::cli::hooks::{
7378
use crate::mcp_client::Prompt;
7479
use crate::os::Os;
7580

81+
use super::tools::todo::TodoState;
82+
7683
const CONTEXT_ENTRY_START_HEADER: &str = "--- CONTEXT ENTRY BEGIN ---\n";
7784
const CONTEXT_ENTRY_END_HEADER: &str = "--- CONTEXT ENTRY END ---\n\n";
7885

@@ -105,6 +112,9 @@ pub struct ConversationState {
105112
/// Model explicitly selected by the user in this conversation state via `/model`.
106113
#[serde(default, skip_serializing_if = "Option::is_none")]
107114
pub model: Option<String>,
115+
116+
// Current todo list
117+
pub todo_state: Option<TodoState>,
108118
}
109119

110120
impl ConversationState {
@@ -157,6 +167,7 @@ impl ConversationState {
157167
context_message_length: None,
158168
latest_summary: None,
159169
model: current_model_id,
170+
todo_state: Some(TodoState::default()),
160171
}
161172
}
162173

@@ -835,6 +846,41 @@ impl ConversationState {
835846
},
836847
}
837848
}
849+
850+
pub async fn create_todo_request(&self, os: &Os, path: PathBuf) -> Result<FigConversationState> {
851+
let contents = self.can_resume_todo(os, &path).await?;
852+
let request = format!(
853+
"[SYSTEM NOTE: This is an automated request, not from the user]
854+
Read the TODO list contents below and understand the task description, completed tasks, and provided context.
855+
Call the Load command of the todo_list tool with the given file path as an argument to display the TODO list to the user and officially resume execution of the TODO list tasks.
856+
TODO LIST CONTENTS: {}
857+
FILE PATH: {}",
858+
contents,
859+
path.display()
860+
);
861+
862+
let request_message = UserInputMessage {
863+
content: request,
864+
user_input_message_context: None,
865+
user_intent: None,
866+
images: None,
867+
model_id: self.model.clone(),
868+
};
869+
870+
Ok(FigConversationState {
871+
conversation_id: Some(self.conversation_id.clone()),
872+
user_input_message: request_message,
873+
history: None,
874+
})
875+
}
876+
877+
// For now, just check that file path is valid and deserializable
878+
// QUESTION: How do you pass errors cleanly in the todo_request functions?
879+
pub async fn can_resume_todo(&self, os: &Os, path: &PathBuf) -> Result<String> {
880+
let contents = os.fs.read_to_string(path).await?;
881+
let _ = serde_json::from_str::<TodoState>(&contents)?;
882+
Ok(contents)
883+
}
838884
}
839885

840886
/// Represents a conversation state that can be converted into a [FigConversationState] (the type

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ use eyre::{
5555
bail,
5656
eyre,
5757
};
58+
use std::path::PathBuf;
59+
5860
use input_source::InputSource;
5961
use message::{
6062
AssistantMessage,
@@ -970,6 +972,64 @@ impl ChatSession {
970972
Ok(())
971973
}
972974

975+
async fn resume_todo(
976+
&mut self,
977+
os: &mut Os,
978+
path: PathBuf,
979+
) -> Result<ChatState, ChatError> {
980+
let request_state = self
981+
.conversation
982+
.create_todo_request(os, path)
983+
.await;
984+
985+
match request_state {
986+
Ok(state) => {
987+
self.conversation.set_next_user_message(state.user_input_message.content).await;
988+
},
989+
Err(_) => {
990+
execute!(
991+
self.stderr,
992+
style::Print("TODO could not be resumed\n"),
993+
)?;
994+
return Ok(ChatState::PromptUser {
995+
skip_printing_tools: true,
996+
});
997+
},
998+
};
999+
1000+
// Use the normal sendable conversation state which includes tool definitions
1001+
let conv_state = self
1002+
.conversation
1003+
.as_sendable_conversation_state(os, &mut self.stderr, false)
1004+
.await?;
1005+
1006+
let response = os.client.send_message(conv_state).await?;
1007+
1008+
let mut result = self.handle_response(os, response).await?;
1009+
match result {
1010+
ChatState::ExecuteTools => {
1011+
result = self.tool_use_execute(os).await?;
1012+
},
1013+
ChatState::ValidateTools(tool_uses) => {
1014+
result = self.validate_tools(os, tool_uses).await?;
1015+
},
1016+
ChatState::HandleResponseStream(response) => {
1017+
result = self.handle_response(os, response).await?;
1018+
},
1019+
other => {
1020+
return Ok(other);
1021+
},
1022+
}
1023+
Ok(result)
1024+
1025+
1026+
// Ok(ChatState::PromptUser {
1027+
// skip_printing_tools: true,
1028+
// })
1029+
1030+
1031+
}
1032+
9731033
/// Compacts the conversation history, replacing the history with a summary generated by the
9741034
/// model.
9751035
///

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -977,7 +977,7 @@ impl ToolManager {
977977
"report_issue" => Tool::GhIssue(serde_json::from_value::<GhIssue>(value.args).map_err(map_err)?),
978978
"thinking" => Tool::Thinking(serde_json::from_value::<Thinking>(value.args).map_err(map_err)?),
979979
"knowledge" => Tool::Knowledge(serde_json::from_value::<Knowledge>(value.args).map_err(map_err)?),
980-
"todoooooo" => Tool::Todo(serde_json::from_value::<TodoInput>(value.args).map_err(map_err)?),
980+
"todo_list" => Tool::Todo(serde_json::from_value::<TodoInput>(value.args).map_err(map_err)?),
981981
// Note that this name is namespaced with server_name{DELIMITER}tool_name
982982
name => {
983983
// Note: tn_map also has tools that underwent no transformation. In otherwords, if

0 commit comments

Comments
 (0)