Skip to content

Commit 3dc87a9

Browse files
feat: add waiting animation for tool uses (#680)
1 parent 6a443a1 commit 3dc87a9

File tree

2 files changed

+45
-23
lines changed

2 files changed

+45
-23
lines changed

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ use std::process::ExitCode;
1515
use std::sync::Arc;
1616
use std::sync::atomic::AtomicBool;
1717

18-
use color_eyre::owo_colors::OwoColorize;
1918
use conversation_state::ConversationState;
2019
use crossterm::style::{
2120
Attribute,
2221
Color,
22+
Stylize,
2323
};
2424
use crossterm::{
2525
cursor,
@@ -289,6 +289,7 @@ Hi, I'm <g>Amazon Q</g>. Ask me anything.
289289
}
290290
}
291291

292+
let mut waiting_for_tool = false;
292293
loop {
293294
match parser.recv().await {
294295
Ok(msg_event) => {
@@ -305,11 +306,31 @@ Hi, I'm <g>Amazon Q</g>. Ask me anything.
305306
std::process::exit(0);
306307
});
307308
},
309+
parser::ResponseEvent::ToolUseStart { name } => {
310+
if self.is_interactive {
311+
execute!(self.output, style::Print("\n\n"))?;
312+
self.spinner = Some(Spinner::new(
313+
Spinners::Dots,
314+
format!("Creating tool {}...", name.green()),
315+
));
316+
}
317+
waiting_for_tool = true;
318+
},
308319
parser::ResponseEvent::AssistantText(text) => {
309320
buf.push_str(&text);
310321
},
311322
parser::ResponseEvent::ToolUse(tool_use) => {
323+
if self.is_interactive && self.spinner.is_some() {
324+
drop(self.spinner.take());
325+
queue!(
326+
self.output,
327+
terminal::Clear(terminal::ClearType::CurrentLine),
328+
cursor::MoveToColumn(0),
329+
cursor::Show
330+
)?;
331+
}
312332
self.tool_uses.push(tool_use);
333+
waiting_for_tool = false;
313334
},
314335
parser::ResponseEvent::EndStream { message } => {
315336
self.conversation_state.push_assistant_message(message);
@@ -352,7 +373,7 @@ Hi, I'm <g>Amazon Q</g>. Ask me anything.
352373
buf.push('\n');
353374
}
354375

355-
if !buf.is_empty() && self.is_interactive && self.spinner.is_some() {
376+
if !waiting_for_tool && !buf.is_empty() && self.is_interactive && self.spinner.is_some() {
356377
drop(self.spinner.take());
357378
queue!(
358379
self.output,

crates/q_cli/src/cli/chat/parser.rs

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use fig_api_client::model::{
99
use tracing::{
1010
error,
1111
trace,
12-
warn,
1312
};
1413

1514
use super::tools::serde_value_to_document;
@@ -57,6 +56,9 @@ pub struct ResponseParser {
5756
buffered_line: Option<String>,
5857
/// Short circuit and return early since we simply need to clear our buffered line
5958
short_circuit: bool,
59+
/// Whether or not we are currently receiving tool use delta events. Tuple of
60+
/// `Some((tool_use_id, name))` if true, [None] otherwise.
61+
parsing_tool_use: Option<(String, String)>,
6062
}
6163

6264
impl ResponseParser {
@@ -69,6 +71,7 @@ impl ResponseParser {
6971
tool_uses: Vec::new(),
7072
buffered_line: None,
7173
short_circuit: false,
74+
parsing_tool_use: None,
7275
}
7376
}
7477

@@ -87,6 +90,12 @@ impl ResponseParser {
8790
return Ok(ResponseEvent::EndStream { message });
8891
}
8992

93+
if let Some((id, name)) = self.parsing_tool_use.take() {
94+
let tool_use = self.parse_tool_use(id, name).await?;
95+
self.tool_uses.push(tool_use.clone());
96+
return Ok(ResponseEvent::ToolUse(tool_use));
97+
}
98+
9099
loop {
91100
match self.next().await {
92101
Ok(Some(output)) => match output {
@@ -121,9 +130,13 @@ impl ResponseParser {
121130
input,
122131
stop,
123132
} => {
124-
let tool_use = self.parse_tool_use(tool_use_id, name, input, stop).await?;
125-
self.tool_uses.push(tool_use.clone());
126-
return Ok(ResponseEvent::ToolUse(tool_use));
133+
debug_assert!(input.is_none(), "Unexpected initial content in first tool use event");
134+
debug_assert!(
135+
stop.is_none_or(|v| !v),
136+
"Unexpected immediate stop in first tool use event"
137+
);
138+
self.parsing_tool_use = Some((tool_use_id.clone(), name.clone()));
139+
return Ok(ResponseEvent::ToolUseStart { name });
127140
},
128141
_ => {},
129142
},
@@ -152,18 +165,8 @@ impl ResponseParser {
152165
/// Consumes the response stream until a valid [ToolUse] is parsed.
153166
///
154167
/// The arguments are the fields from the first [ChatResponseStream::ToolUseEvent] consumed.
155-
async fn parse_tool_use(
156-
&mut self,
157-
tool_use_id: String,
158-
tool_name: String,
159-
input: Option<String>,
160-
stop: Option<bool>,
161-
) -> Result<ToolUse> {
162-
if input.is_some() {
163-
warn!(?input, "Unexpected initial content in input");
164-
}
165-
assert!(stop.is_none_or(|v| !v));
166-
let mut tool_string = input.unwrap_or_default();
168+
async fn parse_tool_use(&mut self, id: String, name: String) -> Result<ToolUse> {
169+
let mut tool_string = String::new();
167170
while let Some(ChatResponseStream::ToolUseEvent { .. }) = self.peek().await? {
168171
if let Some(ChatResponseStream::ToolUseEvent { input, stop, .. }) = self.next().await? {
169172
if let Some(i) = input {
@@ -175,11 +178,7 @@ impl ResponseParser {
175178
}
176179
}
177180
let args = serde_json::from_str(&tool_string)?;
178-
Ok(ToolUse {
179-
id: tool_use_id,
180-
name: tool_name,
181-
args,
182-
})
181+
Ok(ToolUse { id, name, args })
183182
}
184183

185184
/// Returns the next event in the [SendMessageOutput] without consuming it.
@@ -214,6 +213,8 @@ pub enum ResponseEvent {
214213
ConversationId(String),
215214
/// Text returned by the assistant. This should be displayed to the user as it is received.
216215
AssistantText(String),
216+
/// Notification that a tool use is being received.
217+
ToolUseStart { name: String },
217218
/// A tool use requested by the assistant. This should be displayed to the user as it is
218219
/// received.
219220
ToolUse(ToolUse),

0 commit comments

Comments
 (0)