Skip to content

Commit c000ffb

Browse files
author
kiran-garre
committed
feat: Add view subcommand for /todos
Also: - Ran formatter - Resolved most relevant clippy issues - Switched from printing to error handling in todos.rs - Changed /todos subcommand name from "select" to "resume"
1 parent 398bdb4 commit c000ffb

File tree

9 files changed

+188
-180
lines changed

9 files changed

+188
-180
lines changed

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ pub mod persist;
1010
pub mod profile;
1111
pub mod prompts;
1212
pub mod subscribe;
13+
pub mod todos;
1314
pub mod tools;
1415
pub mod usage;
15-
pub mod todos;
1616

1717
use clap::Parser;
1818
use clear::ClearArgs;
@@ -26,8 +26,8 @@ use model::ModelArgs;
2626
use persist::PersistSubcommand;
2727
use profile::ProfileSubcommand;
2828
use prompts::PromptsArgs;
29-
use tools::ToolsArgs;
3029
use todos::TodoSubcommand;
30+
use tools::ToolsArgs;
3131

3232
use crate::cli::chat::cli::subscribe::SubscribeArgs;
3333
use crate::cli::chat::cli::usage::UsageArgs;
@@ -124,7 +124,6 @@ impl SlashCommand {
124124
// skip_printing_tools: true,
125125
// })
126126
// },
127-
128127
Self::Todos(subcommand) => subcommand.execute(os, session).await,
129128
}
130129
}
Lines changed: 96 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
11
use clap::Subcommand;
2-
use crate::{cli::chat::tools::todo::TodoState, os::Os};
3-
use crossterm::{
4-
execute,
5-
style::{self, Stylize},
2+
use crossterm::execute;
3+
use crossterm::style::{
4+
self,
5+
Stylize,
66
};
7+
use dialoguer::FuzzySelect;
8+
use eyre::Result;
79

10+
use crate::cli::chat::tools::todo::TodoState;
811
use crate::cli::chat::{
912
ChatError,
1013
ChatSession,
1114
ChatState,
1215
};
13-
14-
use eyre::Result;
15-
16-
use dialoguer::{
17-
FuzzySelect
18-
};
16+
use crate::os::Os;
1917

2018
#[derive(Debug, PartialEq, Subcommand)]
2119
pub enum TodoSubcommand {
20+
// Show all tracked to-do lists
2221
Show,
22+
23+
// Clear completed to-do lists
2324
ClearFinished,
24-
Select,
25+
26+
// Resume a selected to-do list
27+
Resume,
28+
29+
// View a to-do list
30+
View,
2531
}
2632

2733
/// Used for displaying completed and in-progress todo lists
@@ -35,13 +41,12 @@ pub struct TodoDisplayEntry {
3541
impl std::fmt::Display for TodoDisplayEntry {
3642
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3743
if self.num_completed == self.num_tasks {
38-
write!(f, "{} {}",
39-
"✓".green().bold(),
40-
self.description.clone(),
41-
)
44+
write!(f, "{} {}", "✓".green().bold(), self.description.clone(),)
4245
} else {
43-
write!(f, "{} {} ({}/{})",
44-
"✗".red().bold(),
46+
write!(
47+
f,
48+
"{} {} ({}/{})",
49+
"✗".red().bold(),
4550
self.description.clone(),
4651
self.num_completed,
4752
self.num_tasks
@@ -53,49 +58,32 @@ impl std::fmt::Display for TodoDisplayEntry {
5358
impl TodoSubcommand {
5459
pub async fn execute(self, os: &mut Os, session: &mut ChatSession) -> Result<ChatState, ChatError> {
5560
match self {
56-
Self::Show => {
57-
match self.get_descriptions_and_statuses(os) {
58-
Ok(entries) => {
59-
if entries.len() == 0 {
60-
execute!(
61-
session.stderr,
62-
style::Print("No to-do lists to show"),
63-
)?;
64-
}
65-
for e in entries {
66-
execute!(
67-
session.stderr,
68-
style::Print(e),
69-
style::Print("\n"),
70-
)?;
71-
}
61+
Self::Show => match Self::get_descriptions_and_statuses(os) {
62+
Ok(entries) => {
63+
if entries.is_empty() {
64+
execute!(session.stderr, style::Print("No to-do lists to show"),)?;
7265
}
73-
Err(_) => {
74-
execute!(
75-
session.stderr,
76-
style::Print("Could not show to-do lists"),
77-
)?;
66+
for e in entries {
67+
execute!(session.stderr, style::Print(e), style::Print("\n"),)?;
7868
}
79-
}
69+
},
70+
Err(_) => return Err(ChatError::Custom("Could not show to-do lists".into())),
8071
},
8172
Self::ClearFinished => {
82-
();
73+
8374
},
84-
Self::Select => {
85-
match self.get_descriptions_and_statuses(os) {
75+
Self::Resume => {
76+
match Self::get_descriptions_and_statuses(os) {
8677
Ok(entries) => {
87-
if entries.len() == 0 {
88-
execute!(
89-
session.stderr,
90-
style::Print("No to-do lists to show"),
91-
)?;
78+
if entries.is_empty() {
79+
execute!(session.stderr, style::Print("No to-do lists to show"),)?;
9280
} else {
9381
let selection = FuzzySelect::new()
94-
.with_prompt("Select task:")
82+
.with_prompt("Select task to resume:")
9583
.items(&entries)
9684
.report(false)
9785
.interact_opt()
98-
.unwrap_or(None);
86+
.unwrap_or(None);
9987

10088
if let Some(index) = selection {
10189
if index < entries.len() {
@@ -108,82 +96,116 @@ impl TodoSubcommand {
10896
}
10997
}
11098
}
111-
}
112-
// %%% FIX %%%
113-
Err(e) => println!("{:?}", e),
114-
// %%% --- %%%
99+
},
100+
Err(_) => return Err(ChatError::Custom("Could not show to-do lists".into())),
115101
};
116102
},
117-
};
118-
Ok(ChatState::PromptUser { skip_printing_tools: true })
103+
Self::View => {
104+
match Self::get_descriptions_and_statuses(os) {
105+
Ok(entries) => {
106+
if entries.is_empty() {
107+
execute!(session.stderr, style::Print("No to-do lists to view"))?;
108+
} else {
109+
let selection = FuzzySelect::new()
110+
.with_prompt("Select task to view:")
111+
.items(&entries)
112+
.report(false)
113+
.interact_opt()
114+
.unwrap_or(None);
115+
116+
if let Some(index) = selection {
117+
if index < entries.len() {
118+
let list = match TodoState::load(os, &entries[index].id) {
119+
Ok(list) => list,
120+
Err(_) => {
121+
return Err(ChatError::Custom("Could not load requested to-do list".into()));
122+
}
123+
};
124+
match list.display_list(&mut session.stderr) {
125+
Ok(_) => {},
126+
Err(_) => {
127+
return Err(ChatError::Custom("Could not display requested to-do list".into()));
128+
}
129+
};
130+
execute!(session.stderr, style::Print("\n"),)?;
131+
}
132+
}
133+
}
134+
},
135+
Err(_) => return Err(ChatError::Custom("Could not show to-do lists".into())),
136+
}
137+
}
138+
}
139+
Ok(ChatState::PromptUser {
140+
skip_printing_tools: true,
141+
})
119142
}
120143

121-
/// Convert all to-do list state files to displayable entries
122-
fn get_descriptions_and_statuses(self, os: &Os) -> Result<Vec<TodoDisplayEntry>> {
144+
/// Convert all to-do list state entries to displayable entries
145+
fn get_descriptions_and_statuses(os: &Os) -> Result<Vec<TodoDisplayEntry>> {
123146
let mut out = Vec::new();
124147
let entries = os.database.get_all_todos()?;
125148
for (id, value) in entries.iter() {
126149
let temp_struct = match value.as_str() {
127-
Some(s) => { match serde_json::from_str::<TodoState>(s) {
150+
Some(s) => match serde_json::from_str::<TodoState>(s) {
128151
Ok(state) => state,
129152
Err(_) => continue,
130-
}},
153+
},
131154
None => continue,
132155
};
133156
// For some reason this doesn't work
134-
// Has to do with the Value::String wrapping in os.database.all_entries() rather than Value::from_str()
135-
// let temp_struct = match serde_json::from_value::<TodoState>(value.clone()) {
136-
// Ok(state) => state,
157+
// Has to do with the Value::String wrapping in os.database.all_entries() rather than
158+
// Value::from_str()
159+
// let temp_struct = match
160+
// serde_json::from_value::<TodoState>(value.clone()) { Ok(state) => state,
137161
// Err(_) => continue,
138162
// };
139163

140164
out.push(TodoDisplayEntry {
141165
num_completed: temp_struct.completed.iter().filter(|b| **b).count(),
142166
num_tasks: temp_struct.completed.len(),
143167
description: prewrap(&temp_struct.task_description),
144-
id: id.to_string(),
168+
id: id.clone(),
145169
});
146170
}
147171
Ok(out)
148172
}
149-
150173
}
151174

152-
153175
const MAX_LINE_LENGTH: usize = 80;
154176

155177
// FIX: Hacky workaround for cleanly wrapping lines
156178
/// Insert newlines every n characters, not within a word and not at the end.
157-
///
179+
///
158180
/// Generated by Q
159181
fn prewrap(text: &str) -> String {
160182
if text.is_empty() || MAX_LINE_LENGTH == 0 {
161183
return text.to_string();
162184
}
163-
185+
164186
let mut result = String::new();
165187
let mut current_line_length = 0;
166188
let words: Vec<&str> = text.split_whitespace().collect();
167-
168-
for (_, word) in words.iter().enumerate() {
189+
190+
for word in words.iter() {
169191
let word_length = word.len();
170-
192+
171193
// If adding this word would exceed the line length and we're not at the start of a line
172194
if current_line_length > 0 && current_line_length + 1 + word_length > MAX_LINE_LENGTH {
173195
result.push('\n');
174196
result.push_str(&" ".repeat("> ".len()));
175197
current_line_length = 0;
176198
}
177-
199+
178200
// Add space before word if not at start of line
179201
if current_line_length > 0 {
180202
result.push(' ');
181203
current_line_length += 1;
182204
}
183-
205+
184206
result.push_str(word);
185207
current_line_length += word_length;
186208
}
187-
209+
188210
result
189-
}
211+
}

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ use crossterm::{
1111
execute,
1212
style,
1313
};
14+
use eyre::{
15+
Result,
16+
bail,
17+
};
1418
use serde::{
1519
Deserialize,
1620
Serialize,
@@ -21,11 +25,6 @@ use tracing::{
2125
warn,
2226
};
2327

24-
use eyre::{
25-
Result,
26-
bail,
27-
};
28-
2928
use super::consts::{
3029
DUMMY_TOOL_NAME,
3130
MAX_CHARS,
@@ -46,6 +45,7 @@ use super::token_counter::{
4645
CharCounter,
4746
};
4847
use super::tool_manager::ToolManager;
48+
use super::tools::todo::TodoState;
4949
use super::tools::{
5050
InputSchema,
5151
QueuedTool,
@@ -78,8 +78,6 @@ use crate::cli::chat::cli::hooks::{
7878
use crate::mcp_client::Prompt;
7979
use crate::os::Os;
8080

81-
use super::tools::todo::TodoState;
82-
8381
const CONTEXT_ENTRY_START_HEADER: &str = "--- CONTEXT ENTRY BEGIN ---\n";
8482
const CONTEXT_ENTRY_END_HEADER: &str = "--- CONTEXT ENTRY END ---\n\n";
8583

@@ -850,7 +848,7 @@ impl ConversationState {
850848
pub async fn create_todo_request(&self, os: &Os, id: &str) -> Result<FigConversationState> {
851849
let contents = match os.database.get_todo(id)? {
852850
Some(todo_list) => serde_json::to_string(&todo_list)?,
853-
None => bail!("No todo list with id {}", id)
851+
None => bail!("No todo list with id {}", id),
854852
};
855853
let request = format!(
856854
"[SYSTEM NOTE: This is an automated request, not from the user]\n
@@ -861,8 +859,8 @@ impl ConversationState {
861859
ID: {}\n",
862860
contents,
863861
id
864-
);
865-
862+
);
863+
866864
let request_message = UserInputMessage {
867865
content: request,
868866
user_input_message_context: None,
@@ -875,7 +873,7 @@ impl ConversationState {
875873
conversation_id: Some(self.conversation_id.clone()),
876874
user_input_message: request_message,
877875
history: None,
878-
})
876+
})
879877
}
880878
}
881879

0 commit comments

Comments
 (0)