Skip to content

Commit 76a8197

Browse files
authored
feat(chat ui): brand new greeting UI + rotating tips (#1262)
1 parent 62bcc9d commit 76a8197

File tree

1 file changed

+176
-20
lines changed
  • crates/q_cli/src/cli/chat

1 file changed

+176
-20
lines changed

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

Lines changed: 176 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@ use crossterm::style::{
4242
Color,
4343
Stylize,
4444
};
45+
use crossterm::terminal::ClearType;
4546
use crossterm::{
4647
cursor,
4748
execute,
4849
queue,
4950
style,
5051
terminal,
5152
};
53+
use dialoguer::console::strip_ansi_codes;
5254
use eyre::{
5355
ErrReport,
5456
Result,
@@ -66,7 +68,10 @@ use fig_api_client::model::{
6668
ToolResultStatus,
6769
};
6870
use fig_os_shim::Context;
69-
use fig_settings::Settings;
71+
use fig_settings::{
72+
Settings,
73+
State,
74+
};
7075
use fig_util::CLI_BINARY_NAME;
7176
use hooks::{
7277
Hook,
@@ -151,27 +156,45 @@ use crate::util::token_counter::TokenCounter;
151156

152157
const WELCOME_TEXT: &str = color_print::cstr! {"
153158
154-
<em>Hi, I'm <magenta,em>Amazon Q</magenta,em>. Ask me anything.</em>
155-
156-
<cyan!>Things to try</cyan!>
157-
• Fix the build failures in this project.
158-
• List my s3 buckets in us-west-2.
159-
• Write unit tests for my application.
160-
• Help me understand my git status.
161-
162-
<em>/tools</em> <black!>View and manage tools and permissions</black!>
163-
<em>/issue</em> <black!>Report an issue or make a feature request</black!>
164-
<em>/profile</em> <black!>(Beta) Manage profiles for the chat session</black!>
165-
<em>/context</em> <black!>(Beta) Manage context files and hooks for a profile</black!>
166-
<em>/compact</em> <black!>Summarize the conversation to free up context space</black!>
167-
<em>/help</em> <black!>Show the help dialogue</black!>
168-
<em>/quit</em> <black!>Quit the application</black!>
169-
170-
<cyan!>Use Ctrl(^) + j to provide multi-line prompts.</cyan!>
171-
<cyan!>Use Ctrl(^) + k to fuzzily search commands and context.</cyan!>
159+
<em>Welcome to </em>
160+
<cyan!>
161+
█████╗ ███╗ ███╗ █████╗ ███████╗ ██████╗ ███╗ ██╗ ██████╗
162+
██╔══██╗████╗ ████║██╔══██╗╚══███╔╝██╔═══██╗████╗ ██║ ██╔═══██╗
163+
███████║██╔████╔██║███████║ ███╔╝ ██║ ██║██╔██╗ ██║ ██║ ██║
164+
██╔══██║██║╚██╔╝██║██╔══██║ ███╔╝ ██║ ██║██║╚██╗██║ ██║▄▄ ██║
165+
██║ ██║██║ ╚═╝ ██║██║ ██║███████╗╚██████╔╝██║ ╚████║ ╚██████╔╝
166+
╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚══▀▀═╝
167+
</cyan!>
168+
"};
172169

170+
const SMALL_SCREEN_WECLOME_TEXT: &str = color_print::cstr! {"
171+
<em>Welcome to <cyan!>Amazon Q</cyan!>!</em>
173172
"};
174173

174+
const ROTATING_TIPS: [&str; 7] = [
175+
color_print::cstr! {"You can use <green!>/editor</green!> to edit your prompt with a vim-like experience"},
176+
color_print::cstr! {"You can execute bash commands by typing <green!>!</green!> followed by the command"},
177+
color_print::cstr! {"Q can use tools without asking for confirmation every time. Give <green!>/tools trust</green!> a try"},
178+
color_print::cstr! {"You can programmatically inject context to your prompts by using hooks. Check out <green!>/context hooks help</green!>"},
179+
color_print::cstr! {"You can use <green!>/compact</green!> to replace the conversation history with its summary to free up the context space"},
180+
color_print::cstr! {"<green!>/usage</green!> shows you a visual breakdown of your current context window usage"},
181+
color_print::cstr! {"If you want to file an issue to the Q CLI team, just tell me, or run <green!>q issue</green!>"},
182+
];
183+
184+
const GREETING_BREAK_POINT: usize = 67;
185+
186+
const POPULAR_SHORTCUTS: &str = color_print::cstr! {"
187+
<black!>
188+
<green!>/help</green!> all commands <em>•</em> <green!>ctrl + j</green!> new lines <em>•</em> <green!>ctrl + k</green!> fuzzy search
189+
</black!>"};
190+
191+
const SMALL_SCREEN_POPULAR_SHORTCUTS: &str = color_print::cstr! {"
192+
<black!>
193+
<green!>/help</green!> all commands
194+
<green!>ctrl + j</green!> new lines
195+
<green!>ctrl + k</green!> fuzzy search
196+
</black!>
197+
"};
175198
const HELP_TEXT: &str = color_print::cstr! {"
176199
177200
<magenta,em>q</magenta,em> (Amazon Q Chat)
@@ -310,6 +333,7 @@ pub async fn chat(
310333
let mut chat = ChatContext::new(
311334
ctx,
312335
Settings::new(),
336+
State::new(),
313337
output,
314338
input,
315339
InputSource::new()?,
@@ -362,6 +386,8 @@ pub enum ChatError {
362386
pub struct ChatContext<W: Write> {
363387
ctx: Arc<Context>,
364388
settings: Settings,
389+
/// The [State] to use for the chat context.
390+
state: State,
365391
/// The [Write] destination for printing conversation text.
366392
output: W,
367393
initial_input: Option<String>,
@@ -391,6 +417,7 @@ impl<W: Write> ChatContext<W> {
391417
pub async fn new(
392418
ctx: Arc<Context>,
393419
settings: Settings,
420+
state: State,
394421
output: W,
395422
input: Option<String>,
396423
input_source: InputSource,
@@ -405,6 +432,7 @@ impl<W: Write> ChatContext<W> {
405432
Ok(Self {
406433
ctx,
407434
settings,
435+
state,
408436
output,
409437
initial_input: input,
410438
input_source,
@@ -540,11 +568,135 @@ where
540568
Ok(content.trim().to_string())
541569
}
542570

571+
fn draw_tip_box(&mut self, text: &str) -> Result<()> {
572+
let box_width = GREETING_BREAK_POINT;
573+
let inner_width = box_width - 4; // account for │ and padding
574+
575+
// wrap the single line into multiple lines respecting inner width
576+
// Manually wrap the text by splitting at word boundaries
577+
let mut wrapped_lines = Vec::new();
578+
let mut line = String::new();
579+
580+
for word in text.split_whitespace() {
581+
if line.len() + word.len() < inner_width {
582+
if !line.is_empty() {
583+
line.push(' ');
584+
}
585+
line.push_str(word);
586+
} else {
587+
wrapped_lines.push(line);
588+
line = word.to_string();
589+
}
590+
}
591+
592+
if !line.is_empty() {
593+
wrapped_lines.push(line);
594+
}
595+
596+
// ───── Did you know? ─────
597+
let label = " Did you know? ";
598+
let side_len = (box_width.saturating_sub(label.len())) / 2;
599+
let top_border = format!(
600+
"╭{}{}{}╮",
601+
"─".repeat(side_len - 1),
602+
label,
603+
"─".repeat(box_width - side_len - label.len() - 1)
604+
);
605+
606+
// Build output
607+
execute!(
608+
self.output,
609+
terminal::Clear(ClearType::CurrentLine),
610+
cursor::MoveToColumn(0),
611+
style::Print(format!("{top_border}\n")),
612+
)?;
613+
614+
// Top vertical padding
615+
execute!(
616+
self.output,
617+
style::Print(format!("│{: <width$}│\n", "", width = box_width - 2))
618+
)?;
619+
620+
// Centered wrapped content
621+
for line in wrapped_lines {
622+
let visible_line_len = strip_ansi_codes(&line).len();
623+
let left_pad = (box_width - 4 - visible_line_len) / 2;
624+
625+
let content = format!(
626+
"│ {: <pad$}{}{: <rem$} │",
627+
"",
628+
line,
629+
"",
630+
pad = left_pad,
631+
rem = box_width - 4 - left_pad - visible_line_len
632+
);
633+
execute!(self.output, style::Print(format!("{}\n", content)))?;
634+
}
635+
636+
// Bottom vertical padding
637+
execute!(
638+
self.output,
639+
style::Print(format!("│{: <width$}│\n", "", width = box_width - 2))
640+
)?;
641+
642+
// Bottom rounded corner line: ╰────────────╯
643+
let bottom = format!("╰{}╯", "─".repeat(box_width - 2));
644+
execute!(self.output, style::Print(format!("{}\n", bottom)))?;
645+
646+
Ok(())
647+
}
648+
543649
async fn try_chat(&mut self) -> Result<()> {
650+
let is_small_screen = self.terminal_width() < GREETING_BREAK_POINT;
544651
if self.interactive && self.settings.get_bool_or("chat.greeting.enabled", true) {
545-
execute!(self.output, style::Print(WELCOME_TEXT))?;
652+
execute!(
653+
self.output,
654+
style::Print(if is_small_screen {
655+
SMALL_SCREEN_WECLOME_TEXT
656+
} else {
657+
WELCOME_TEXT
658+
}),
659+
style::Print("\n\n"),
660+
)?;
661+
662+
let current_tip_index =
663+
(self.state.get_int_or("chat.greeting.rotating_tips_current_index", 0) as usize) % ROTATING_TIPS.len();
664+
665+
let tip = ROTATING_TIPS[current_tip_index];
666+
if is_small_screen {
667+
// If the screen is small, print the tip in a single line
668+
execute!(
669+
self.output,
670+
style::Print("💡 ".to_string()),
671+
style::Print(tip),
672+
style::Print("\n")
673+
)?;
674+
} else {
675+
self.draw_tip_box(tip)?;
676+
}
677+
678+
// update the current tip index
679+
let next_tip_index = (current_tip_index + 1) % ROTATING_TIPS.len();
680+
self.state
681+
.set_value("chat.greeting.rotating_tips_current_index", next_tip_index)?;
546682
}
547683

684+
execute!(
685+
self.output,
686+
style::Print(if is_small_screen {
687+
SMALL_SCREEN_POPULAR_SHORTCUTS
688+
} else {
689+
POPULAR_SHORTCUTS
690+
}),
691+
style::Print(
692+
"━"
693+
.repeat(if is_small_screen { 0 } else { GREETING_BREAK_POINT })
694+
.dark_grey()
695+
)
696+
)?;
697+
execute!(self.output, style::Print("\n"), style::SetForegroundColor(Color::Reset))?;
698+
self.output.flush()?;
699+
548700
let mut ctrl_c_stream = signal(SignalKind::interrupt())?;
549701

550702
let mut next_state = Some(ChatState::PromptUser {
@@ -2924,6 +3076,7 @@ mod tests {
29243076
ChatContext::new(
29253077
Arc::clone(&ctx),
29263078
Settings::new_fake(),
3079+
State::new_fake(),
29273080
std::io::stdout(),
29283081
None,
29293082
InputSource::new_mock(vec![
@@ -3047,6 +3200,7 @@ mod tests {
30473200
ChatContext::new(
30483201
Arc::clone(&ctx),
30493202
Settings::new_fake(),
3203+
State::new_fake(),
30503204
std::io::stdout(),
30513205
None,
30523206
InputSource::new_mock(vec![
@@ -3145,6 +3299,7 @@ mod tests {
31453299
ChatContext::new(
31463300
Arc::clone(&ctx),
31473301
Settings::new_fake(),
3302+
State::new_fake(),
31483303
std::io::stdout(),
31493304
None,
31503305
InputSource::new_mock(vec![
@@ -3215,6 +3370,7 @@ mod tests {
32153370
ChatContext::new(
32163371
Arc::clone(&ctx),
32173372
Settings::new_fake(),
3373+
State::new_fake(),
32183374
std::io::stdout(),
32193375
None,
32203376
InputSource::new_mock(vec![

0 commit comments

Comments
 (0)