diff --git a/crates/q_cli/src/cli/chat/command.rs b/crates/q_cli/src/cli/chat/command.rs index 84f6b8a965..3784026763 100644 --- a/crates/q_cli/src/cli/chat/command.rs +++ b/crates/q_cli/src/cli/chat/command.rs @@ -1,5 +1,9 @@ use std::io::Write; +use clap::{ + Parser, + Subcommand, +}; use crossterm::style::Color; use crossterm::{ queue, @@ -87,6 +91,57 @@ Profiles allow you to organize and manage different sets of context files for di } } +#[derive(Parser, Debug, Clone)] +#[command(name = "hooks", disable_help_flag = true, disable_help_subcommand = true)] +struct HooksCommand { + #[command(subcommand)] + command: HooksSubcommand, +} + +#[derive(Subcommand, Debug, Clone, Eq, PartialEq)] +pub enum HooksSubcommand { + Add { + name: String, + + #[arg(long, value_parser = ["per_prompt", "conversation_start"])] + r#type: String, + + #[arg(long, value_parser = clap::value_parser!(String))] + command: String, + + #[arg(long)] + global: bool, + }, + #[command(name = "rm")] + Remove { + name: String, + + #[arg(long)] + global: bool, + }, + Enable { + name: String, + + #[arg(long)] + global: bool, + }, + Disable { + name: String, + + #[arg(long)] + global: bool, + }, + EnableAll { + #[arg(long)] + global: bool, + }, + DisableAll { + #[arg(long)] + global: bool, + }, + Help, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum ContextSubcommand { Show { @@ -104,6 +159,9 @@ pub enum ContextSubcommand { Clear { global: bool, }, + Hooks { + subcommand: Option, + }, Help, } @@ -124,8 +182,32 @@ impl ContextSubcommand { --global: Remove specified rules globally clear [--global] Remove all rules from current profile - --global: Remove global rules"}; + --global: Remove global rules + + hooks View and manage context hooks"}; const CLEAR_USAGE: &str = "/context clear [--global]"; + const HOOKS_AVAILABLE_COMMANDS: &str = color_print::cstr! {"Available subcommands + hooks help Show an explanation for context hooks commands + + hooks add [--global] <> Add a new command context hook + --global: Add to global hooks + --type <> Type of hook, valid options: `per_prompt` or `conversation_start` + --command <> Shell command to execute + + hooks rm [--global] <> Remove an existing context hook + --global: Remove from global hooks + + hooks enable [--global] <> Enable an existing context hook + --global: Enable in global hooks + + hooks disable [--global] <> Disable an existing context hook + --global: Disable in global hooks + + hooks enable-all [--global] Enable all existing context hooks + --global: Enable all in global hooks + + hooks disable-all [--global] Disable all existing context hooks + --global: Disable all in global hooks"}; const REMOVE_USAGE: &str = "/context rm [--global] [path2...]"; const SHOW_USAGE: &str = "/context show [--expand]"; @@ -133,6 +215,10 @@ impl ContextSubcommand { format!("{}\n\n{}", header.as_ref(), Self::AVAILABLE_COMMANDS) } + fn hooks_usage_msg(header: impl AsRef) -> String { + format!("{}\n\n{}", header.as_ref(), Self::HOOKS_AVAILABLE_COMMANDS) + } + pub fn help_text() -> String { color_print::cformat!( r#" @@ -143,6 +229,9 @@ The files matched by these rules provide Amazon Q with additional information about your project or environment. Adding relevant files helps Q generate more accurate and helpful responses. +In addition to files, you can specify hooks that will run commands and return +the output as context to Amazon Q. + {} Notes @@ -154,6 +243,32 @@ more accurate and helpful responses. Self::AVAILABLE_COMMANDS ) } + + pub fn hooks_help_text() -> String { + color_print::cformat!( + r#" +(Beta) Context Hooks + +Use context hooks to specify shell commands to run. The output from these +commands will be appended to the prompt to Amazon Q. Hooks can be defined +in global or local profiles. + +Usage: /context hooks [SUBCOMMAND] + +Description + Show existing global or profile-specific hooks. + Alternatively, specify a subcommand to modify the hooks. + +{} + +Notes +• Hooks are executed in parallel +• 'conversation_start' hooks run on the first user prompt and are attached once to the conversation history sent to Amazon Q +• 'per_prompt' hooks run on each user prompt and are attached to the prompt +"#, + Self::HOOKS_AVAILABLE_COMMANDS + ) + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -493,6 +608,18 @@ impl Command { "help" => Self::Context { subcommand: ContextSubcommand::Help, }, + "hooks" => { + if parts.get(2).is_none() { + return Ok(Self::Context { + subcommand: ContextSubcommand::Hooks { subcommand: None }, + }); + }; + + match Self::parse_hooks(&parts) { + Ok(command) => command, + Err(err) => return Err(ContextSubcommand::hooks_usage_msg(err)), + } + }, other => { return Err(ContextSubcommand::usage_msg(format!("Unknown subcommand '{}'.", other))); }, @@ -567,6 +694,27 @@ impl Command { prompt: input.to_string(), }) } + + // NOTE: Here we use clap to parse the hooks subcommand instead of parsing manually + // like the rest of the file. + // Since the hooks subcommand has a lot of options, this makes more sense. + // Ideally, we parse everything with clap instead of trying to do it manually. + fn parse_hooks(parts: &[&str]) -> Result { + // Skip the first two parts ("/context" and "hooks") + let args = match shlex::split(&parts[1..].join(" ")) { + Some(args) => args, + None => return Err("Failed to parse arguments".to_string()), + }; + + // Parse with Clap + HooksCommand::try_parse_from(args) + .map(|hooks_command| Self::Context { + subcommand: ContextSubcommand::Hooks { + subcommand: Some(hooks_command.command), + }, + }) + .map_err(|e| e.to_string()) + } } #[cfg(test)] @@ -688,6 +836,66 @@ mod tests { ("/issue \"there was an error in the chat\"", Command::Issue { prompt: Some("\"there was an error in the chat\"".to_string()), }), + ( + "/context hooks", + context!(ContextSubcommand::Hooks { subcommand: None }), + ), + ( + "/context hooks add test --type per_prompt --command 'echo 1' --global", + context!(ContextSubcommand::Hooks { + subcommand: Some(HooksSubcommand::Add { + name: "test".to_string(), + global: true, + r#type: "per_prompt".to_string(), + command: "echo 1".to_string() + }) + }), + ), + ( + "/context hooks rm test --global", + context!(ContextSubcommand::Hooks { + subcommand: Some(HooksSubcommand::Remove { + name: "test".to_string(), + global: true + }) + }), + ), + ( + "/context hooks enable test --global", + context!(ContextSubcommand::Hooks { + subcommand: Some(HooksSubcommand::Enable { + name: "test".to_string(), + global: true + }) + }), + ), + ( + "/context hooks disable test", + context!(ContextSubcommand::Hooks { + subcommand: Some(HooksSubcommand::Disable { + name: "test".to_string(), + global: false + }) + }), + ), + ( + "/context hooks enable-all --global", + context!(ContextSubcommand::Hooks { + subcommand: Some(HooksSubcommand::EnableAll { global: true }) + }), + ), + ( + "/context hooks disable-all", + context!(ContextSubcommand::Hooks { + subcommand: Some(HooksSubcommand::DisableAll { global: false }) + }), + ), + ( + "/context hooks help", + context!(ContextSubcommand::Hooks { + subcommand: Some(HooksSubcommand::Help) + }), + ), ]; for (input, parsed) in tests { diff --git a/crates/q_cli/src/cli/chat/conversation_state.rs b/crates/q_cli/src/cli/chat/conversation_state.rs index 7d053470ac..6ce38a9bab 100644 --- a/crates/q_cli/src/cli/chat/conversation_state.rs +++ b/crates/q_cli/src/cli/chat/conversation_state.rs @@ -137,19 +137,24 @@ impl ConversationState { } } - pub async fn append_new_user_message(&mut self, input: String) { + pub async fn append_new_user_message(&mut self, input: String, extra_context: Option) { debug_assert!(self.next_message.is_none(), "next_message should not exist"); if let Some(next_message) = self.next_message.as_ref() { warn!(?next_message, "next_message should not exist"); } - let input = if input.is_empty() { + let mut input = if input.is_empty() { warn!("input must not be empty when adding new messages"); "Empty prompt".to_string() } else { input }; + // Context from hooks (scripts, commands, tools) + if let Some(context) = extra_context { + input = format!("{} {}", context, input); + } + let msg = UserInputMessage { content: input, user_input_message_context: Some(UserInputMessageContext { @@ -370,14 +375,14 @@ impl ConversationState { /// Returns a [FigConversationState] capable of being sent by /// [fig_api_client::StreamingClient] while preparing the current conversation state to be sent /// in the next message. - pub async fn as_sendable_conversation_state(&mut self) -> FigConversationState { + pub async fn as_sendable_conversation_state(&mut self, extra_context: Option) -> FigConversationState { debug_assert!(self.next_message.is_some()); self.fix_history(); // The current state we want to send let mut curr_state = self.clone(); - if let Some((user, assistant)) = self.context_messages().await { + if let Some((user, assistant)) = self.context_messages(extra_context).await { self.context_message_length = Some(user.content.len()); curr_state .history @@ -410,9 +415,11 @@ impl ConversationState { /// Returns a pair of user and assistant messages to include as context in the message history /// including both summaries and context files if available. - pub async fn context_messages(&self) -> Option<(UserInputMessage, AssistantResponseMessage)> { + pub async fn context_messages( + &mut self, + extra_context: Option, + ) -> Option<(UserInputMessage, AssistantResponseMessage)> { let mut context_content = String::new(); - let mut has_content = false; // Add summary if available - emphasize its importance more strongly if let Some(summary) = &self.latest_summary { @@ -422,11 +429,10 @@ impl ConversationState { context_content.push_str("SUMMARY CONTENT:\n"); context_content.push_str(summary); context_content.push_str("\n--- END SUMMARY - YOU MUST USE THIS INFORMATION IN YOUR RESPONSES ---\n\n"); - has_content = true; } // Add context files if available - if let Some(context_manager) = &self.context_manager { + if let Some(context_manager) = self.context_manager.as_mut() { match context_manager.get_context_files(true).await { Ok(files) => { if !files.is_empty() { @@ -435,7 +441,6 @@ impl ConversationState { context_content.push_str(&format!("[{}]\n{}\n", filename, content)); } context_content.push_str("--- CONTEXT FILES END ---\n\n"); - has_content = true; } }, Err(e) => { @@ -443,8 +448,11 @@ impl ConversationState { }, } } + if let Some(extra_context) = extra_context { + context_content.push_str(&extra_context); + } - if has_content { + if !context_content.is_empty() { let user_msg = UserInputMessage { content: format!( "Here is critical information you MUST consider when answering questions:\n\n{}", @@ -732,16 +740,18 @@ mod tests { // First, build a large conversation history. We need to ensure that the order is always // User -> Assistant -> User -> Assistant ...and so on. - conversation_state.append_new_user_message("start".to_string()).await; + conversation_state + .append_new_user_message("start".to_string(), None) + .await; for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) { - let s = conversation_state.as_sendable_conversation_state().await; + let s = conversation_state.as_sendable_conversation_state(None).await; assert_conversation_state_invariants(s, i); conversation_state.push_assistant_message(AssistantResponseMessage { message_id: None, content: i.to_string(), tool_uses: None, }); - conversation_state.append_new_user_message(i.to_string()).await; + conversation_state.append_new_user_message(i.to_string(), None).await; } } @@ -749,9 +759,11 @@ mod tests { async fn test_conversation_state_history_handling_with_tool_results() { // Build a long conversation history of tool use results. let mut conversation_state = ConversationState::new(Context::new_fake(), load_tools().unwrap(), None).await; - conversation_state.append_new_user_message("start".to_string()).await; + conversation_state + .append_new_user_message("start".to_string(), None) + .await; for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) { - let s = conversation_state.as_sendable_conversation_state().await; + let s = conversation_state.as_sendable_conversation_state(None).await; assert_conversation_state_invariants(s, i); conversation_state.push_assistant_message(AssistantResponseMessage { message_id: None, @@ -771,9 +783,11 @@ mod tests { // Build a long conversation history of user messages mixed in with tool results. let mut conversation_state = ConversationState::new(Context::new_fake(), load_tools().unwrap(), None).await; - conversation_state.append_new_user_message("start".to_string()).await; + conversation_state + .append_new_user_message("start".to_string(), None) + .await; for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) { - let s = conversation_state.as_sendable_conversation_state().await; + let s = conversation_state.as_sendable_conversation_state(None).await; assert_conversation_state_invariants(s, i); if i % 3 == 0 { conversation_state.push_assistant_message(AssistantResponseMessage { @@ -796,7 +810,7 @@ mod tests { content: i.to_string(), tool_uses: None, }); - conversation_state.append_new_user_message(i.to_string()).await; + conversation_state.append_new_user_message(i.to_string(), None).await; } } } @@ -810,9 +824,11 @@ mod tests { // First, build a large conversation history. We need to ensure that the order is always // User -> Assistant -> User -> Assistant ...and so on. - conversation_state.append_new_user_message("start".to_string()).await; + conversation_state + .append_new_user_message("start".to_string(), None) + .await; for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) { - let s = conversation_state.as_sendable_conversation_state().await; + let s = conversation_state.as_sendable_conversation_state(None).await; // Ensure that the first two messages are the fake context messages. let hist = s.history.as_ref().unwrap(); @@ -836,7 +852,52 @@ mod tests { content: i.to_string(), tool_uses: None, }); - conversation_state.append_new_user_message(i.to_string()).await; + conversation_state.append_new_user_message(i.to_string(), None).await; + } + } + + #[tokio::test] + async fn test_conversation_state_additional_context() { + let ctx = Context::builder().with_test_home().await.unwrap().build_fake(); + let mut conversation_state = ConversationState::new(ctx, load_tools().unwrap(), None).await; + + let conversation_start_context = "conversation start context"; + let prompt_context = "prompt context"; + + // Simulate conversation flow + conversation_state + .append_new_user_message("start".to_string(), Some(prompt_context.to_string())) + .await; + for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) { + let s = conversation_state + .as_sendable_conversation_state(Some(conversation_start_context.to_string())) + .await; + let hist = s.history.as_ref().unwrap(); + match &hist[0] { + ChatMessage::UserInputMessage(user) => { + assert!( + user.content.contains(conversation_start_context), + "expected to contain '{conversation_start_context}', instead found: {}", + user.content + ); + }, + #[allow(clippy::match_wildcard_for_single_variants)] + _ => panic!("Expected user message."), + } + assert!( + s.user_input_message.content.contains(prompt_context), + "expected to contain '{prompt_context}', instead found: {}", + s.user_input_message.content + ); + + conversation_state.push_assistant_message(AssistantResponseMessage { + message_id: None, + content: i.to_string(), + tool_uses: None, + }); + conversation_state + .append_new_user_message(i.to_string(), Some(prompt_context.to_string())) + .await; } } } diff --git a/crates/q_cli/src/cli/chat/mod.rs b/crates/q_cli/src/cli/chat/mod.rs index 25347c09d8..3bcfc0884e 100644 --- a/crates/q_cli/src/cli/chat/mod.rs +++ b/crates/q_cli/src/cli/chat/mod.rs @@ -49,6 +49,7 @@ use crossterm::{ terminal, }; use eyre::{ + ErrReport, Result, bail, }; @@ -66,6 +67,7 @@ use fig_api_client::model::{ use fig_os_shim::Context; use fig_settings::Settings; use fig_util::CLI_BINARY_NAME; +use hooks::Hook; use summarization_state::{ SummarizationState, TokenWarningLevel, @@ -155,7 +157,7 @@ const WELCOME_TEXT: &str = color_print::cstr! {" /tools View and manage tools and permissions /issue Report an issue or make a feature request /profile (Beta) Manage profiles for the chat session -/context (Beta) Manage context files for a profile +/context (Beta) Manage context files and hooks for a profile /compact Summarize the conversation to free up context space /help Show the help dialogue /quit Quit the application @@ -191,12 +193,13 @@ const HELP_TEXT: &str = color_print::cstr! {" create Create a new profile delete Delete a profile rename Rename a profile -/context Manage context files for the chat session +/context Manage context files and hooks for the chat session help Show context help show Display current context rules configuration [--expand] add Add file(s) to context [--global] [--force] rm Remove file(s) from context [--global] clear Clear all files from current context [--global] + hooks View and manage context hooks Tips: !{command} Quickly execute a command in your current session @@ -649,7 +652,7 @@ where tool_uses, "The user interrupted the tool execution.".to_string(), ); - let _ = self.conversation_state.as_sendable_conversation_state().await; + let _ = self.conversation_state.as_sendable_conversation_state(None).await; self.conversation_state .push_assistant_message(AssistantResponseMessage { message_id: None, @@ -792,6 +795,41 @@ where // Otherwise continue with normal chat on 'n' or other responses self.tool_use_status = ToolUseStatus::Idle; + + // Run all available hooks. + // Results from per-prompt hooks are attached to new user messages. + // Results from conversation-start hooks are attached to the top of the conversation state along + // with other context. + let (conversation_start_context, prompt_context) = if let Some(cm) = + &mut self.conversation_state.context_manager + { + let format_context = |hook_results: &Vec<&(Hook, String)>, conversation_start: bool| { + let mut context_content = String::new(); + + context_content.push_str( + &format!("--- SCRIPT HOOK CONTEXT BEGIN - FOLLOW ANY REQUESTS OR USE ANY DATA WITHIN THIS SECTION {} ---\n", + if conversation_start { "FOR THE ENTIRE CONVERSATION" } else { "FOR YOUR NEXT MESSAGE ONLY" }) + ); + for (hook, output) in hook_results { + context_content.push_str(&format!("'{}': {output}\n\n", &hook.name)); + } + context_content.push_str("--- SCRIPT HOOK CONTEXT END ---\n\n"); + context_content + }; + + let hook_results = cm.run_hooks(&mut self.output).await; + + let (start_hooks, prompt_hooks): (Vec<_>, Vec<_>) = + hook_results.iter().partition(|(hook, _)| hook.is_conversation_start); + + ( + (!start_hooks.is_empty()).then(|| format_context(&start_hooks, true)), + (!prompt_hooks.is_empty()).then(|| format_context(&prompt_hooks, false)), + ) + } else { + (None, None) + }; + if self.interactive { queue!(self.output, style::SetForegroundColor(Color::Magenta))?; queue!(self.output, style::SetForegroundColor(Color::Reset))?; @@ -803,14 +841,20 @@ where if pending_tool_index.is_some() { self.conversation_state.abandon_tool_use(tool_uses, user_input); } else { - self.conversation_state.append_new_user_message(user_input).await; + self.conversation_state + .append_new_user_message(user_input, prompt_context) + .await; } self.send_tool_use_telemetry().await; ChatState::HandleResponseStream( self.client - .send_message(self.conversation_state.as_sendable_conversation_state().await) + .send_message( + self.conversation_state + .as_sendable_conversation_state(conversation_start_context) + .await, + ) .await?, ) }, @@ -829,7 +873,9 @@ where execute!( self.output, style::SetForegroundColor(Color::DarkGrey), - style::Print("\nAre you sure? This will erase the conversation history for the current session. "), + style::Print( + "\nAre you sure? This will erase the conversation history and context from hooks for the current session. " + ), style::Print("["), style::SetForegroundColor(Color::Green), style::Print("y"), @@ -850,6 +896,9 @@ where if ["y", "Y"].contains(&user_input.as_str()) { self.conversation_state.clear(true); + if let Some(cm) = self.conversation_state.context_manager.as_mut() { + cm.hook_executor.execution_cache.clear(); + } execute!( self.output, style::SetForegroundColor(Color::Green), @@ -952,7 +1001,9 @@ where }; // Add the summarization request - self.conversation_state.append_new_user_message(summary_request).await; + self.conversation_state + .append_new_user_message(summary_request, None) + .await; // Use spinner while we wait if self.interactive { @@ -963,7 +1014,7 @@ where // Return to handle response stream state return Ok(ChatState::HandleResponseStream( self.client - .send_message(self.conversation_state.as_sendable_conversation_state().await) + .send_message(self.conversation_state.as_sendable_conversation_state(None).await) .await?, )); }, @@ -1410,6 +1461,196 @@ where style::Print("\n") )?; }, + command::ContextSubcommand::Hooks { subcommand } => { + fn map_chat_error(e: ErrReport) -> ChatError { + ChatError::Custom(e.to_string().into()) + } + + let scope = |g: bool| if g { "global" } else { "profile" }; + if let Some(subcommand) = subcommand { + match subcommand { + command::HooksSubcommand::Add { + name, + r#type, + command, + global, + } => { + context_manager + .add_hook( + Hook::new_inline_hook(&name, command, false), + global, + r#type == "conversation_start", + ) + .await + .map_err(map_chat_error)?; + execute!( + self.output, + style::SetForegroundColor(Color::Green), + style::Print(format!("\nAdded {} hook '{name}'.\n\n", scope(global))), + style::SetForegroundColor(Color::Reset) + )?; + }, + command::HooksSubcommand::Remove { name, global } => { + context_manager + .remove_hook(&name, global) + .await + .map_err(map_chat_error)?; + execute!( + self.output, + style::SetForegroundColor(Color::Green), + style::Print(format!("\nRemoved {} hook '{name}'.\n\n", scope(global))), + style::SetForegroundColor(Color::Reset) + )?; + }, + command::HooksSubcommand::Enable { name, global } => { + context_manager + .set_hook_disabled(&name, global, false) + .await + .map_err(map_chat_error)?; + execute!( + self.output, + style::SetForegroundColor(Color::Green), + style::Print(format!("\nEnabled {} hook '{name}'.\n\n", scope(global))), + style::SetForegroundColor(Color::Reset) + )?; + }, + command::HooksSubcommand::Disable { name, global } => { + context_manager + .set_hook_disabled(&name, global, true) + .await + .map_err(map_chat_error)?; + execute!( + self.output, + style::SetForegroundColor(Color::Green), + style::Print(format!("\nDisabled {} hook '{name}'.\n\n", scope(global))), + style::SetForegroundColor(Color::Reset) + )?; + }, + command::HooksSubcommand::EnableAll { global } => { + context_manager + .set_all_hooks_disabled(global, false) + .await + .map_err(map_chat_error)?; + execute!( + self.output, + style::SetForegroundColor(Color::Green), + style::Print(format!("\nEnabled all {} hooks.\n\n", scope(global))), + style::SetForegroundColor(Color::Reset) + )?; + }, + command::HooksSubcommand::DisableAll { global } => { + context_manager + .set_all_hooks_disabled(global, true) + .await + .map_err(map_chat_error)?; + execute!( + self.output, + style::SetForegroundColor(Color::Green), + style::Print(format!("\nDisabled all {} hooks.\n\n", scope(global))), + style::SetForegroundColor(Color::Reset) + )?; + }, + command::HooksSubcommand::Help => { + execute!( + self.output, + style::Print("\n"), + style::Print(command::ContextSubcommand::hooks_help_text()), + style::Print("\n") + )?; + }, + } + } else { + fn print_hook_section( + output: &mut impl Write, + hooks: &Vec, + conversation_start: bool, + ) -> Result<()> { + let section = if conversation_start { + "Conversation start" + } else { + "Per prompt" + }; + queue!( + output, + style::SetForegroundColor(Color::Cyan), + style::Print(format!(" {section}:\n")), + style::SetForegroundColor(Color::Reset), + )?; + + if hooks.is_empty() { + queue!( + output, + style::SetForegroundColor(Color::DarkGrey), + style::Print(" \n"), + style::SetForegroundColor(Color::Reset) + )?; + } else { + for hook in hooks { + if hook.disabled { + queue!( + output, + style::SetForegroundColor(Color::DarkGrey), + style::Print(format!(" {} (disabled)\n", hook.name)), + style::SetForegroundColor(Color::Reset) + )?; + } else { + queue!(output, style::Print(format!(" {}\n", hook.name)),)?; + } + } + } + Ok(()) + } + queue!( + self.output, + style::SetAttribute(Attribute::Bold), + style::SetForegroundColor(Color::Magenta), + style::Print("\nšŸŒ global:\n"), + style::SetAttribute(Attribute::Reset), + )?; + + print_hook_section( + &mut self.output, + &context_manager.global_config.hooks.conversation_start, + true, + ) + .map_err(map_chat_error)?; + print_hook_section( + &mut self.output, + &context_manager.global_config.hooks.per_prompt, + false, + ) + .map_err(map_chat_error)?; + + queue!( + self.output, + style::SetAttribute(Attribute::Bold), + style::SetForegroundColor(Color::Magenta), + style::Print(format!("\nšŸ‘¤ profile ({}):\n", &context_manager.current_profile)), + style::SetAttribute(Attribute::Reset), + )?; + + print_hook_section( + &mut self.output, + &context_manager.profile_config.hooks.conversation_start, + true, + ) + .map_err(map_chat_error)?; + print_hook_section( + &mut self.output, + &context_manager.profile_config.hooks.per_prompt, + false, + ) + .map_err(map_chat_error)?; + + execute!( + self.output, + style::Print(format!( + "\nUse {} to manage hooks.\n\n", + "/context hooks help".to_string().dark_green() + )), + )?; + } + }, } // fig_telemetry::send_context_command_executed } else { @@ -1665,7 +1906,7 @@ where self.send_tool_use_telemetry().await; return Ok(ChatState::HandleResponseStream( self.client - .send_message(self.conversation_state.as_sendable_conversation_state().await) + .send_message(self.conversation_state.as_sendable_conversation_state(None).await) .await?, )); } @@ -1751,12 +1992,13 @@ where .append_new_user_message( "You took too long to respond - try to split up the work into smaller steps." .to_string(), + None, ) .await; self.send_tool_use_telemetry().await; return Ok(ChatState::HandleResponseStream( self.client - .send_message(self.conversation_state.as_sendable_conversation_state().await) + .send_message(self.conversation_state.as_sendable_conversation_state(None).await) .await?, )); }, @@ -1789,7 +2031,7 @@ where self.send_tool_use_telemetry().await; return Ok(ChatState::HandleResponseStream( self.client - .send_message(self.conversation_state.as_sendable_conversation_state().await) + .send_message(self.conversation_state.as_sendable_conversation_state(None).await) .await?, )); }, @@ -2087,7 +2329,7 @@ where let response = self .client - .send_message(self.conversation_state.as_sendable_conversation_state().await) + .send_message(self.conversation_state.as_sendable_conversation_state(None).await) .await?; return Ok(ChatState::HandleResponseStream(response)); }