Skip to content

Commit 6d13844

Browse files
authored
fix: chat infinite loop and missing lines (#392)
Fixes #390
1 parent dc3af8c commit 6d13844

File tree

3 files changed

+100
-36
lines changed

3 files changed

+100
-36
lines changed

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

+81-24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
mod api;
22
mod parse;
33
mod prompt;
4-
mod terminal;
4+
mod stdio;
55

66
use std::io::{
77
IsTerminal,
@@ -16,13 +16,13 @@ use color_eyre::owo_colors::OwoColorize;
1616
use crossterm::style::{
1717
Attribute,
1818
Color,
19-
Print,
2019
};
2120
use crossterm::{
2221
cursor,
2322
execute,
2423
queue,
2524
style,
25+
terminal,
2626
};
2727
use eyre::{
2828
Result,
@@ -40,7 +40,7 @@ use spinners::{
4040
Spinner,
4141
Spinners,
4242
};
43-
use terminal::StdioOutput;
43+
use stdio::StdioOutput;
4444
use winnow::Partial;
4545
use winnow::stream::Offset;
4646

@@ -79,7 +79,8 @@ pub async fn chat(mut input: String) -> Result<ExitCode> {
7979
}
8080

8181
let mut output = StdioOutput::new(is_interactive);
82-
let result = try_chat(&mut output, input, is_interactive).await;
82+
let client = StreamingClient::new().await?;
83+
let result = try_chat(&mut output, input, is_interactive, &client).await;
8384

8485
if is_interactive {
8586
queue!(output, style::SetAttribute(Attribute::Reset), style::ResetColor).ok();
@@ -89,9 +90,13 @@ pub async fn chat(mut input: String) -> Result<ExitCode> {
8990
result.map(|_| ExitCode::SUCCESS)
9091
}
9192

92-
async fn try_chat<W: Write>(output: &mut W, mut input: String, interactive: bool) -> Result<()> {
93+
async fn try_chat<W: Write>(
94+
output: &mut W,
95+
mut input: String,
96+
interactive: bool,
97+
client: &StreamingClient,
98+
) -> Result<()> {
9399
let mut rl = if interactive { Some(rl()?) } else { None };
94-
let client = StreamingClient::new().await?;
95100
let mut rx = None;
96101
let mut conversation_id: Option<String> = None;
97102
let mut message_id = None;
@@ -158,8 +163,8 @@ You can include additional context by adding the following to your prompt:
158163
let mut offset = 0;
159164
let mut ended = false;
160165

161-
let columns = crossterm::terminal::window_size()?.columns.into();
162-
let mut state = ParseState::new(columns);
166+
let terminal_width = terminal::window_size().map(|s| s.columns.into()).ok();
167+
let mut state = ParseState::new(terminal_width);
163168

164169
loop {
165170
if let Some(response) = rx.recv().await {
@@ -229,11 +234,11 @@ You can include additional context by adding the following to your prompt:
229234
buf.push('\n');
230235
}
231236

232-
if !buf.is_empty() && interactive {
237+
if !buf.is_empty() && interactive && spinner.is_some() {
233238
drop(spinner.take());
234239
queue!(
235240
output,
236-
crossterm::terminal::Clear(crossterm::terminal::ClearType::CurrentLine),
241+
terminal::Clear(terminal::ClearType::CurrentLine),
237242
cursor::MoveToColumn(0),
238243
cursor::Show
239244
)?;
@@ -259,32 +264,26 @@ You can include additional context by adding the following to your prompt:
259264
}
260265

261266
if ended {
267+
if let (Some(conversation_id), Some(message_id)) = (&conversation_id, &message_id) {
268+
fig_telemetry::send_chat_added_message(conversation_id.to_owned(), message_id.to_owned()).await;
269+
}
270+
262271
if interactive {
263-
queue!(
264-
output,
265-
style::ResetColor,
266-
style::SetAttribute(Attribute::Reset),
267-
Print("\n")
268-
)?;
272+
queue!(output, style::ResetColor, style::SetAttribute(Attribute::Reset))?;
269273

270274
for (i, citation) in &state.citations {
271275
queue!(
272276
output,
277+
style::Print("\n"),
273278
style::SetForegroundColor(Color::Blue),
274-
style::Print(format!("{i} ")),
279+
style::Print(format!("[^{i}]: ")),
275280
style::SetForegroundColor(Color::DarkGrey),
276281
style::Print(format!("{citation}\n")),
277282
style::SetForegroundColor(Color::Reset)
278283
)?;
279284
}
280285

281-
if !state.citations.is_empty() {
282-
execute!(output, Print("\n"))?;
283-
}
284-
}
285-
286-
if let (Some(conversation_id), Some(message_id)) = (&conversation_id, &message_id) {
287-
fig_telemetry::send_chat_added_message(conversation_id.to_owned(), message_id.to_owned()).await;
286+
execute!(output, style::Print("\n"))?;
288287
}
289288

290289
break;
@@ -313,6 +312,64 @@ You can include additional context by adding the following to your prompt:
313312
},
314313
}
315314
}
315+
} else {
316+
break Ok(());
316317
}
317318
}
318319
}
320+
321+
#[cfg(test)]
322+
mod test {
323+
use fig_api_client::model::ChatResponseStream;
324+
325+
use super::*;
326+
327+
fn mock_client(s: impl IntoIterator<Item = &'static str>) -> StreamingClient {
328+
StreamingClient::mock(
329+
s.into_iter()
330+
.map(|s| ChatResponseStream::AssistantResponseEvent { content: s.into() })
331+
.collect(),
332+
)
333+
}
334+
335+
#[tokio::test]
336+
async fn try_chat_non_interactive() {
337+
let client = mock_client(["Hello,", " World", "!"]);
338+
let mut output = Vec::new();
339+
try_chat(&mut output, "test".into(), false, &client).await.unwrap();
340+
341+
let mut expected = Vec::new();
342+
execute!(
343+
expected,
344+
style::Print("Hello, World!"),
345+
style::ResetColor,
346+
style::SetAttribute(Attribute::Reset),
347+
style::Print("\n")
348+
)
349+
.unwrap();
350+
351+
assert_eq!(expected, output);
352+
}
353+
354+
#[tokio::test]
355+
async fn try_chat_non_interactive_citation() {
356+
let client = mock_client(["Citation [[1]](https://aws.com)"]);
357+
let mut output = Vec::new();
358+
try_chat(&mut output, "test".into(), false, &client).await.unwrap();
359+
360+
let mut expected = Vec::new();
361+
execute!(
362+
expected,
363+
style::Print("Citation "),
364+
style::SetForegroundColor(Color::Blue),
365+
style::Print("[^1]"),
366+
style::ResetColor,
367+
style::ResetColor,
368+
style::SetAttribute(Attribute::Reset),
369+
style::Print("\n")
370+
)
371+
.unwrap();
372+
373+
assert_eq!(expected, output);
374+
}
375+
}

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

+19-12
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ const BLOCKQUOTE_COLOR: Color = Color::DarkGrey;
5454
const URL_TEXT_COLOR: Color = Color::Blue;
5555
const URL_LINK_COLOR: Color = Color::DarkGrey;
5656

57+
const DEFAULT_RULE_WIDTH: usize = 40;
58+
5759
#[derive(Debug, thiserror::Error)]
5860
pub enum Error<'a> {
5961
#[error(transparent)]
@@ -82,7 +84,7 @@ impl<'a> ParserError<Partial<&'a str>> for Error<'a> {
8284

8385
#[derive(Debug)]
8486
pub struct ParseState {
85-
pub terminal_width: usize,
87+
pub terminal_width: Option<usize>,
8688
pub column: usize,
8789
pub in_codeblock: bool,
8890
pub bold: bool,
@@ -94,7 +96,7 @@ pub struct ParseState {
9496
}
9597

9698
impl ParseState {
97-
pub fn new(terminal_width: usize) -> Self {
99+
pub fn new(terminal_width: Option<usize>) -> Self {
98100
Self {
99101
terminal_width,
100102
column: 0,
@@ -270,7 +272,8 @@ fn horizontal_rule<'a, 'b>(
270272
state.column = 0;
271273
state.set_newline = true;
272274

273-
queue(&mut o, style::Print(format!("{}\n", "━".repeat(state.terminal_width))))
275+
let rule_width = state.terminal_width.unwrap_or(DEFAULT_RULE_WIDTH);
276+
queue(&mut o, style::Print(format!("{}\n", "━".repeat(rule_width))))
274277
}
275278
}
276279

@@ -394,7 +397,7 @@ fn citation<'a, 'b>(
394397

395398
queue_newline_or_advance(&mut o, state, num.width() + 1)?;
396399
queue(&mut o, style::SetForegroundColor(URL_TEXT_COLOR))?;
397-
queue(&mut o, style::Print(format!("[{num}]")))?;
400+
queue(&mut o, style::Print(format!("[^{num}]")))?;
398401
queue(&mut o, style::ResetColor)
399402
}
400403
}
@@ -499,13 +502,17 @@ fn queue_newline_or_advance<'a, 'b>(
499502
state: &'b mut ParseState,
500503
width: usize,
501504
) -> Result<(), ErrMode<Error<'a>>> {
502-
if state.column > 0 && state.column + width > state.terminal_width {
503-
state.column = width;
504-
queue(&mut o, style::Print('\n'))?;
505-
} else {
506-
state.column += width;
505+
if let Some(terminal_width) = state.terminal_width {
506+
if state.column > 0 && state.column + width > terminal_width {
507+
state.column = width;
508+
queue(&mut o, style::Print('\n'))?;
509+
return Ok(());
510+
}
507511
}
508512

513+
// else
514+
state.column += width;
515+
509516
Ok(())
510517
}
511518

@@ -630,7 +637,7 @@ mod tests {
630637
input.push(' ');
631638
input.push(' ');
632639

633-
let mut state = ParseState::new(256);
640+
let mut state = ParseState::new(Some(80));
634641
let mut presult = vec![];
635642
let mut offset = 0;
636643

@@ -686,7 +693,7 @@ mod tests {
686693
]);
687694
validate!(citation_1, "[[1]](google.com)", [
688695
style::SetForegroundColor(URL_TEXT_COLOR),
689-
style::Print("[1]"),
696+
style::Print("[^1]"),
690697
style::ResetColor,
691698
]);
692699
validate!(bold_1, "**hello**", [
@@ -709,7 +716,7 @@ mod tests {
709716
validate!(ampersand_1, "&amp;", [style::Print('&')]);
710717
validate!(quote_1, "&quot;", [style::Print('"')]);
711718
validate!(fallback_1, "+ % @ . ? ", [style::Print("+ % @ . ?")]);
712-
validate!(horizontal_rule_1, "---", [style::Print("━".repeat(256))]);
719+
validate!(horizontal_rule_1, "---", [style::Print("━".repeat(80))]);
713720
validate!(heading_1, "# Hello World", [
714721
style::SetForegroundColor(HEADING_COLOR),
715722
style::SetAttribute(Attribute::Bold),
File renamed without changes.

0 commit comments

Comments
 (0)