From 967ce49316af0c7613d7e394278de11b740da241 Mon Sep 17 00:00:00 2001 From: abhraina-aws Date: Thu, 21 Aug 2025 15:34:51 -0700 Subject: [PATCH 1/8] feat: add tangent mode for context isolated conversations (#2634) - Add /tangent command to toggle between normal and tangent modes - Preserve conversation history during tangent for context - Restore main conversation when exiting tangent mode - Add Ctrl+T keyboard shortcut for quick access - Visual indicator: green [T] prompt in tangent mode - Comprehensive tests for state management and UI components Allows users to explore side topics without polluting main conversation context. --- crates/chat-cli/src/cli/chat/cli/mod.rs | 18 +- crates/chat-cli/src/cli/chat/cli/tangent.rs | 66 ++++++ crates/chat-cli/src/cli/chat/conversation.rs | 111 ++++++++++ crates/chat-cli/src/cli/chat/mod.rs | 206 ++++++------------ crates/chat-cli/src/cli/chat/prompt.rs | 99 ++++++--- crates/chat-cli/src/cli/chat/prompt_parser.rs | 82 +++++-- crates/chat-cli/src/database/settings.rs | 14 +- crates/chat-cli/src/telemetry/core.rs | 2 + 8 files changed, 402 insertions(+), 196 deletions(-) create mode 100644 crates/chat-cli/src/cli/chat/cli/tangent.rs diff --git a/crates/chat-cli/src/cli/chat/cli/mod.rs b/crates/chat-cli/src/cli/chat/cli/mod.rs index 4805426d06..4bebac0593 100644 --- a/crates/chat-cli/src/cli/chat/cli/mod.rs +++ b/crates/chat-cli/src/cli/chat/cli/mod.rs @@ -10,6 +10,7 @@ pub mod persist; pub mod profile; pub mod prompts; pub mod subscribe; +pub mod tangent; pub mod tools; pub mod usage; @@ -25,17 +26,13 @@ use model::ModelArgs; use persist::PersistSubcommand; use profile::AgentSubcommand; use prompts::PromptsArgs; +use tangent::TangentArgs; use tools::ToolsArgs; use crate::cli::chat::cli::subscribe::SubscribeArgs; use crate::cli::chat::cli::usage::UsageArgs; use crate::cli::chat::consts::AGENT_MIGRATION_DOC_URL; -use crate::cli::chat::{ - ChatError, - ChatSession, - ChatState, - EXTRA_HELP, -}; +use crate::cli::chat::{ChatError, ChatSession, ChatState, EXTRA_HELP}; use crate::cli::issue; use crate::os::Os; @@ -81,6 +78,8 @@ pub enum SlashCommand { Model(ModelArgs), /// Upgrade to a Q Developer Pro subscription for increased query limits Subscribe(SubscribeArgs), + /// Toggle tangent mode for isolated conversations + Tangent(TangentArgs), #[command(flatten)] Persist(PersistSubcommand), // #[command(flatten)] @@ -94,10 +93,7 @@ impl SlashCommand { Self::Clear(args) => args.execute(session).await, Self::Agent(subcommand) => subcommand.execute(os, session).await, Self::Profile => { - use crossterm::{ - execute, - style, - }; + use crossterm::{execute, style}; execute!( session.stderr, style::SetForegroundColor(style::Color::Yellow), @@ -136,6 +132,7 @@ impl SlashCommand { Self::Mcp(args) => args.execute(session).await, Self::Model(args) => args.execute(os, session).await, Self::Subscribe(args) => args.execute(os, session).await, + Self::Tangent(args) => args.execute(os, session).await, Self::Persist(subcommand) => subcommand.execute(os, session).await, // Self::Root(subcommand) => { // if let Err(err) = subcommand.execute(os, database, telemetry).await { @@ -167,6 +164,7 @@ impl SlashCommand { Self::Mcp(_) => "mcp", Self::Model(_) => "model", Self::Subscribe(_) => "subscribe", + Self::Tangent(_) => "tangent", Self::Persist(sub) => match sub { PersistSubcommand::Save { .. } => "save", PersistSubcommand::Load { .. } => "load", diff --git a/crates/chat-cli/src/cli/chat/cli/tangent.rs b/crates/chat-cli/src/cli/chat/cli/tangent.rs new file mode 100644 index 0000000000..5c1e32f325 --- /dev/null +++ b/crates/chat-cli/src/cli/chat/cli/tangent.rs @@ -0,0 +1,66 @@ +use clap::Args; +use crossterm::execute; +use crossterm::style::{self, Color}; + +use crate::cli::chat::{ChatError, ChatSession, ChatState}; +use crate::os::Os; + +#[derive(Debug, PartialEq, Args)] +pub struct TangentArgs; + +impl TangentArgs { + pub async fn execute(self, os: &Os, session: &mut ChatSession) -> Result { + if session.conversation.is_in_tangent_mode() { + session.conversation.exit_tangent_mode(); + execute!( + session.stderr, + style::SetForegroundColor(Color::DarkGrey), + style::Print("Restored conversation from checkpoint ("), + style::SetForegroundColor(Color::Yellow), + style::Print("↯"), + style::SetForegroundColor(Color::DarkGrey), + style::Print("). - Returned to main conversation.\n"), + style::SetForegroundColor(Color::Reset) + )?; + } else { + session.conversation.enter_tangent_mode(); + + // Get the configured tangent mode key for display + let tangent_key_char = match os + .database + .settings + .get_string(crate::database::settings::Setting::TangentModeKey) + { + Some(key) if key.len() == 1 => key.chars().next().unwrap_or('t'), + _ => 't', // Default to 't' if setting is missing or invalid + }; + let tangent_key_display = format!("ctrl + {}", tangent_key_char.to_lowercase()); + + execute!( + session.stderr, + style::SetForegroundColor(Color::DarkGrey), + style::Print("Created a conversation checkpoint ("), + style::SetForegroundColor(Color::Yellow), + style::Print("↯"), + style::SetForegroundColor(Color::DarkGrey), + style::Print("). Use "), + style::SetForegroundColor(Color::Green), + style::Print(&tangent_key_display), + style::SetForegroundColor(Color::DarkGrey), + style::Print(" or "), + style::SetForegroundColor(Color::Green), + style::Print("/tangent"), + style::SetForegroundColor(Color::DarkGrey), + style::Print(" to restore the conversation later.\n"), + style::Print( + "Note: this functionality is experimental and may change or be removed in the future.\n" + ), + style::SetForegroundColor(Color::Reset) + )?; + } + + Ok(ChatState::PromptUser { + skip_printing_tools: false, + }) + } +} diff --git a/crates/chat-cli/src/cli/chat/conversation.rs b/crates/chat-cli/src/cli/chat/conversation.rs index ef1bf241b6..06c998210c 100644 --- a/crates/chat-cli/src/cli/chat/conversation.rs +++ b/crates/chat-cli/src/cli/chat/conversation.rs @@ -126,6 +126,19 @@ pub struct ConversationState { pub file_line_tracker: HashMap, #[serde(default = "default_true")] pub mcp_enabled: bool, + /// Tangent mode checkpoint - stores main conversation when in tangent mode + #[serde(default, skip_serializing_if = "Option::is_none")] + tangent_state: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ConversationCheckpoint { + /// Main conversation history stored while in tangent mode + main_history: VecDeque, + /// Main conversation next message + main_next_message: Option, + /// Main conversation transcript + main_transcript: VecDeque, } impl ConversationState { @@ -184,6 +197,7 @@ impl ConversationState { model_info: model, file_line_tracker: HashMap::new(), mcp_enabled, + tangent_state: None, } } @@ -204,6 +218,42 @@ impl ConversationState { } } + /// Check if currently in tangent mode + pub fn is_in_tangent_mode(&self) -> bool { + self.tangent_state.is_some() + } + + /// Create a checkpoint of current conversation state + fn create_checkpoint(&self) -> ConversationCheckpoint { + ConversationCheckpoint { + main_history: self.history.clone(), + main_next_message: self.next_message.clone(), + main_transcript: self.transcript.clone(), + } + } + + /// Restore conversation state from checkpoint + fn restore_from_checkpoint(&mut self, checkpoint: ConversationCheckpoint) { + self.history = checkpoint.main_history; + self.next_message = checkpoint.main_next_message; + self.transcript = checkpoint.main_transcript; + self.valid_history_range = (0, self.history.len()); + } + + /// Enter tangent mode - creates checkpoint of current state + pub fn enter_tangent_mode(&mut self) { + if self.tangent_state.is_none() { + self.tangent_state = Some(self.create_checkpoint()); + } + } + + /// Exit tangent mode - restore from checkpoint + pub fn exit_tangent_mode(&mut self) { + if let Some(checkpoint) = self.tangent_state.take() { + self.restore_from_checkpoint(checkpoint); + } + } + /// Appends a collection prompts into history and returns the last message in the collection. /// It asserts that the collection ends with a prompt that assumes the role of user. pub fn append_prompts(&mut self, mut prompts: VecDeque) -> Option { @@ -1289,4 +1339,65 @@ mod tests { conversation.set_next_user_message(i.to_string()).await; } } + + #[tokio::test] + async fn test_tangent_mode() { + let mut os = Os::new().await.unwrap(); + let agents = Agents::default(); + let mut tool_manager = ToolManager::default(); + let mut conversation = ConversationState::new( + "fake_conv_id", + agents, + tool_manager.load_tools(&mut os, &mut vec![]).await.unwrap(), + tool_manager, + None, + &os, + false, // mcp_enabled + ) + .await; + + // Initially not in tangent mode + assert!(!conversation.is_in_tangent_mode()); + + // Add some main conversation history + conversation.set_next_user_message("main conversation".to_string()).await; + conversation.push_assistant_message(&mut os, AssistantMessage::new_response(None, "main response".to_string()), None); + conversation.transcript.push_back("main transcript".to_string()); + + let main_history_len = conversation.history.len(); + let main_transcript_len = conversation.transcript.len(); + + // Enter tangent mode (toggle from normal to tangent) + conversation.enter_tangent_mode(); + assert!(conversation.is_in_tangent_mode()); + + // History should be preserved for tangent (not cleared) + assert_eq!(conversation.history.len(), main_history_len); + assert_eq!(conversation.transcript.len(), main_transcript_len); + assert!(conversation.next_message.is_none()); + + // Add tangent conversation + conversation.set_next_user_message("tangent conversation".to_string()).await; + conversation.push_assistant_message(&mut os, AssistantMessage::new_response(None, "tangent response".to_string()), None); + + // During tangent mode, history should have grown + assert_eq!(conversation.history.len(), main_history_len + 1); + assert_eq!(conversation.transcript.len(), main_transcript_len + 1); + + // Exit tangent mode (toggle from tangent to normal) + conversation.exit_tangent_mode(); + assert!(!conversation.is_in_tangent_mode()); + + // Main conversation should be restored (tangent additions discarded) + assert_eq!(conversation.history.len(), main_history_len); // Back to original length + assert_eq!(conversation.transcript.len(), main_transcript_len); // Back to original length + assert!(conversation.transcript.contains(&"main transcript".to_string())); + assert!(!conversation.transcript.iter().any(|t| t.contains("tangent"))); + + // Test multiple toggles + conversation.enter_tangent_mode(); + assert!(conversation.is_in_tangent_mode()); + conversation.exit_tangent_mode(); + assert!(!conversation.is_in_tangent_mode()); + } } diff --git a/crates/chat-cli/src/cli/chat/mod.rs b/crates/chat-cli/src/cli/chat/mod.rs index a0c1779318..5abaa31f07 100644 --- a/crates/chat-cli/src/cli/chat/mod.rs +++ b/crates/chat-cli/src/cli/chat/mod.rs @@ -19,109 +19,40 @@ pub mod tool_manager; pub mod tools; pub mod util; use std::borrow::Cow; -use std::collections::{ - HashMap, - VecDeque, -}; -use std::io::{ - IsTerminal, - Read, - Write, -}; +use std::collections::{HashMap, VecDeque}; +use std::io::{IsTerminal, Read, Write}; use std::process::ExitCode; use std::sync::Arc; -use std::time::{ - Duration, - Instant, -}; +use std::time::{Duration, Instant}; use amzn_codewhisperer_client::types::SubscriptionStatus; -use clap::{ - Args, - CommandFactory, - Parser, -}; +use clap::{Args, CommandFactory, Parser}; use cli::compact::CompactStrategy; -use cli::model::{ - get_available_models, - select_model, -}; +use cli::model::{get_available_models, select_model}; pub use conversation::ConversationState; use conversation::TokenWarningLevel; -use crossterm::style::{ - Attribute, - Color, - Stylize, -}; -use crossterm::{ - cursor, - execute, - queue, - style, - terminal, -}; -use eyre::{ - Report, - Result, - bail, - eyre, -}; +use crossterm::style::{Attribute, Color, Stylize}; +use crossterm::{cursor, execute, queue, style, terminal}; +use tool_manager::{PromptQuery, PromptQueryResult}; +use eyre::{Report, Result, bail, eyre}; use input_source::InputSource; -use message::{ - AssistantMessage, - AssistantToolUse, - ToolUseResult, - ToolUseResultBlock, -}; -use parse::{ - ParseState, - interpret_markdown, -}; -use parser::{ - RecvErrorKind, - RequestMetadata, - SendMessageStream, -}; +use message::{AssistantMessage, AssistantToolUse, ToolUseResult, ToolUseResultBlock}; +use parse::{ParseState, interpret_markdown}; +use parser::{RecvErrorKind, RequestMetadata, SendMessageStream}; use regex::Regex; -use spinners::{ - Spinner, - Spinners, -}; +use spinners::{Spinner, Spinners}; use thiserror::Error; use time::OffsetDateTime; use token_counter::TokenCounter; use tokio::signal::ctrl_c; -use tokio::sync::{ - Mutex, - broadcast, -}; -use tool_manager::{ - PromptQuery, - PromptQueryResult, - ToolManager, - ToolManagerBuilder, -}; +use tokio::sync::{Mutex, broadcast}; +use tool_manager::{ToolManager, ToolManagerBuilder}; use tools::gh_issue::GhIssueContext; -use tools::{ - NATIVE_TOOLS, - OutputKind, - QueuedTool, - Tool, - ToolSpec, -}; -use tracing::{ - debug, - error, - info, - trace, - warn, -}; +use tools::{NATIVE_TOOLS, OutputKind, QueuedTool, Tool, ToolSpec}; +use tracing::{debug, error, info, trace, warn}; use util::images::RichImageBlock; use util::ui::draw_box; -use util::{ - animate_output, - play_notification_bell, -}; +use util::{animate_output, play_notification_bell}; use winnow::Partial; use winnow::stream::Offset; @@ -130,36 +61,22 @@ use super::agent::{ PermissionEvalResult, }; use crate::api_client::model::ToolResultStatus; -use crate::api_client::{ - self, - ApiClientError, -}; +use crate::api_client::{self, ApiClientError}; use crate::auth::AuthError; use crate::auth::builder_id::is_idc_user; use crate::cli::agent::Agents; use crate::cli::chat::cli::SlashCommand; use crate::cli::chat::cli::model::find_model; -use crate::cli::chat::cli::prompts::{ - GetPromptError, - PromptsSubcommand, -}; +use crate::cli::chat::cli::prompts::{GetPromptError, PromptsSubcommand}; use crate::cli::chat::util::sanitize_unicode_tags; use crate::database::settings::Setting; use crate::mcp_client::Prompt; use crate::os::Os; use crate::telemetry::core::{ - AgentConfigInitArgs, - ChatAddedMessageParams, - ChatConversationType, - MessageMetaTag, - RecordUserTurnCompletionArgs, + AgentConfigInitArgs, ChatAddedMessageParams, ChatConversationType, MessageMetaTag, RecordUserTurnCompletionArgs, ToolUseEventBuilder, }; -use crate::telemetry::{ - ReasonCode, - TelemetryResult, - get_error_reason, -}; +use crate::telemetry::{ReasonCode, TelemetryResult, get_error_reason}; use crate::util::MCP_SERVER_TOOL_DELIMITER; const LIMIT_REACHED_TEXT: &str = color_print::cstr! { "You've used all your free requests for this month. You have two options: @@ -177,6 +94,8 @@ pub const EXTRA_HELP: &str = color_print::cstr! {" Ctrl(^) + s Fuzzy search commands and context files Use Tab to select multiple items Change the keybind using: q settings chat.skimCommandKey x +Ctrl(^) + t Toggle tangent mode for isolated conversations + Change the keybind using: q settings chat.tangentModeKey x chat.editMode The prompt editing mode (vim or emacs) Change using: q settings chat.skimCommandKey x "}; @@ -270,13 +189,17 @@ impl ChatArgs { agents.trust_all_tools = self.trust_all_tools; os.telemetry - .send_agent_config_init(&os.database, conversation_id.clone(), AgentConfigInitArgs { - agents_loaded_count: md.load_count as i64, - agents_loaded_failed_count: md.load_failed_count as i64, - legacy_profile_migration_executed: md.migration_performed, - legacy_profile_migrated_count: md.migrated_count as i64, - launched_agent: md.launched_agent, - }) + .send_agent_config_init( + &os.database, + conversation_id.clone(), + AgentConfigInitArgs { + agents_loaded_count: md.load_count as i64, + agents_loaded_failed_count: md.load_failed_count as i64, + legacy_profile_migration_executed: md.migration_performed, + legacy_profile_migrated_count: md.migrated_count as i64, + launched_agent: md.launched_agent, + }, + ) .await .map_err(|err| error!(?err, "failed to send agent config init telemetry")) .ok(); @@ -401,7 +324,7 @@ const SMALL_SCREEN_WELCOME_TEXT: &str = color_print::cstr! {"Welcome to Picking up where we left off..."}; // Only show the model-related tip for now to make users aware of this feature. -const ROTATING_TIPS: [&str; 16] = [ +const ROTATING_TIPS: [&str; 17] = [ color_print::cstr! {"You can resume the last conversation from your current directory by launching with q chat --resume"}, color_print::cstr! {"Get notified whenever Q CLI finishes responding. @@ -433,6 +356,7 @@ const ROTATING_TIPS: [&str; 16] = [ color_print::cstr! {"Use /model to select the model to use for this conversation"}, color_print::cstr! {"Set a default model by running q settings chat.defaultModel MODEL. Run /model to learn more."}, color_print::cstr! {"Run /prompts to learn how to build & run repeatable workflows"}, + color_print::cstr! {"Use /tangent or ctrl + t (customizable) to start isolated conversations ( ↯ ) that don't affect your main chat history"}, ]; const GREETING_BREAK_POINT: usize = 80; @@ -2640,7 +2564,8 @@ impl ChatSession { fn generate_tool_trust_prompt(&mut self) -> String { let profile = self.conversation.current_profile().map(|s| s.to_string()); let all_trusted = self.all_tools_trusted(); - prompt::generate_prompt(profile.as_deref(), all_trusted) + let tangent_mode = self.conversation.is_in_tangent_mode(); + prompt::generate_prompt(profile.as_deref(), all_trusted, tangent_mode) } async fn send_tool_use_telemetry(&mut self, os: &Os) { @@ -2742,7 +2667,13 @@ impl ChatSession { tool_use_id: self.conversation.latest_tool_use_ids(), tool_name: self.conversation.latest_tool_use_names(), assistant_response_length: md.map(|md| md.response_size as i32), - message_meta_tags: md.map(|md| md.message_meta_tags.clone()).unwrap_or_default(), + message_meta_tags: { + let mut tags = md.map(|md| md.message_meta_tags.clone()).unwrap_or_default(); + if self.conversation.is_in_tangent_mode() { + tags.push(crate::telemetry::core::MessageMetaTag::TangentMode); + } + tags + }, }; os.telemetry .send_chat_added_message(&os.database, conversation_id.clone(), result, data) @@ -2762,26 +2693,31 @@ impl ChatSession { }; os.telemetry - .send_record_user_turn_completion(&os.database, conversation_id, result, RecordUserTurnCompletionArgs { - message_ids: mds.iter().map(|md| md.message_id.clone()).collect::<_>(), - request_ids: mds.iter().map(|md| md.request_id.clone()).collect::<_>(), - reason, - reason_desc, - status_code, - time_to_first_chunks_ms: mds - .iter() - .map(|md| md.time_to_first_chunk.map(|d| d.as_secs_f64() * 1000.0)) - .collect::<_>(), - chat_conversation_type: md.and_then(|md| md.chat_conversation_type), - assistant_response_length: mds.iter().map(|md| md.response_size as i64).sum(), - message_meta_tags: mds.last().map(|md| md.message_meta_tags.clone()).unwrap_or_default(), - user_prompt_length: mds.first().map(|md| md.user_prompt_length).unwrap_or_default() as i64, - user_turn_duration_seconds, - follow_up_count: mds - .iter() - .filter(|md| matches!(md.chat_conversation_type, Some(ChatConversationType::ToolUse))) - .count() as i64, - }) + .send_record_user_turn_completion( + &os.database, + conversation_id, + result, + RecordUserTurnCompletionArgs { + message_ids: mds.iter().map(|md| md.message_id.clone()).collect::<_>(), + request_ids: mds.iter().map(|md| md.request_id.clone()).collect::<_>(), + reason, + reason_desc, + status_code, + time_to_first_chunks_ms: mds + .iter() + .map(|md| md.time_to_first_chunk.map(|d| d.as_secs_f64() * 1000.0)) + .collect::<_>(), + chat_conversation_type: md.and_then(|md| md.chat_conversation_type), + assistant_response_length: mds.iter().map(|md| md.response_size as i64).sum(), + message_meta_tags: mds.last().map(|md| md.message_meta_tags.clone()).unwrap_or_default(), + user_prompt_length: mds.first().map(|md| md.user_prompt_length).unwrap_or_default() as i64, + user_turn_duration_seconds, + follow_up_count: mds + .iter() + .filter(|md| matches!(md.chat_conversation_type, Some(ChatConversationType::ToolUse))) + .count() as i64, + }, + ) .await .ok(); } diff --git a/crates/chat-cli/src/cli/chat/prompt.rs b/crates/chat-cli/src/cli/chat/prompt.rs index b785faa1d9..a2a50bddc9 100644 --- a/crates/chat-cli/src/cli/chat/prompt.rs +++ b/crates/chat-cli/src/cli/chat/prompt.rs @@ -2,36 +2,14 @@ use std::borrow::Cow; use std::cell::RefCell; use eyre::Result; -use rustyline::completion::{ - Completer, - FilenameCompleter, - extract_word, -}; +use rustyline::completion::{Completer, FilenameCompleter, extract_word}; use rustyline::error::ReadlineError; -use rustyline::highlight::{ - CmdKind, - Highlighter, -}; +use rustyline::highlight::{CmdKind, Highlighter}; use rustyline::hint::Hinter as RustylineHinter; use rustyline::history::DefaultHistory; -use rustyline::validate::{ - ValidationContext, - ValidationResult, - Validator, -}; +use rustyline::validate::{ValidationContext, ValidationResult, Validator}; use rustyline::{ - Cmd, - Completer, - CompletionType, - Config, - Context, - EditMode, - Editor, - EventHandler, - Helper, - Hinter, - KeyCode, - KeyEvent, + Cmd, Completer, CompletionType, Config, Context, EditMode, Editor, EventHandler, Helper, Hinter, KeyCode, KeyEvent, Modifiers, }; use winnow::stream::AsChar; @@ -389,17 +367,22 @@ impl Highlighter for ChatHelper { if let Some(components) = parse_prompt_components(prompt) { let mut result = String::new(); - // Add profile part if present + // Add profile part if present (cyan) if let Some(profile) = components.profile { result.push_str(&format!("[{}] ", profile).cyan().to_string()); } - // Add warning symbol if present + // Add tangent indicator if present (yellow) + if components.tangent_mode { + result.push_str(&"↯ ".yellow().to_string()); + } + + // Add warning symbol if present (red) if components.warning { result.push_str(&"!".red().to_string()); } - // Add the prompt symbol + // Add the prompt symbol (magenta) result.push_str(&"> ".magenta().to_string()); Cow::Owned(result) @@ -458,6 +441,16 @@ pub fn rl( EventHandler::Simple(Cmd::CompleteHint), ); + // Add custom keybinding for Ctrl+T to toggle tangent mode (configurable) + let tangent_key_char = match os.database.settings.get_string(Setting::TangentModeKey) { + Some(key) if key.len() == 1 => key.chars().next().unwrap_or('t'), + _ => 't', // Default to 't' if setting is missing or invalid + }; + rl.bind_sequence( + KeyEvent(KeyCode::Char(tangent_key_char), Modifiers::CTRL), + EventHandler::Simple(Cmd::Insert(1, "/tangent".to_string())), + ); + Ok(rl) } @@ -592,6 +585,54 @@ mod tests { assert_eq!(highlighted, invalid_prompt); } + #[test] + fn test_highlight_prompt_tangent_mode() { + let (prompt_request_sender, _) = tokio::sync::broadcast::channel::(1); + let (_, prompt_response_receiver) = tokio::sync::broadcast::channel::(1); + let helper = ChatHelper { + completer: ChatCompleter::new(prompt_request_sender, prompt_response_receiver), + hinter: ChatHinter::new(true), + validator: MultiLineValidator, + }; + + // Test tangent mode prompt highlighting - ↯ yellow, > magenta + let highlighted = helper.highlight_prompt("↯ > ", true); + assert_eq!(highlighted, format!("{}{}", "↯ ".yellow(), "> ".magenta())); + } + + #[test] + fn test_highlight_prompt_tangent_mode_with_warning() { + let (prompt_request_sender, _) = tokio::sync::broadcast::channel::(1); + let (_, prompt_response_receiver) = tokio::sync::broadcast::channel::(1); + let helper = ChatHelper { + completer: ChatCompleter::new(prompt_request_sender, prompt_response_receiver), + hinter: ChatHinter::new(true), + validator: MultiLineValidator, + }; + + // Test tangent mode with warning - ↯ yellow, ! red, > magenta + let highlighted = helper.highlight_prompt("↯ !> ", true); + assert_eq!(highlighted, format!("{}{}{}", "↯ ".yellow(), "!".red(), "> ".magenta())); + } + + #[test] + fn test_highlight_prompt_profile_with_tangent_mode() { + let (prompt_request_sender, _) = tokio::sync::broadcast::channel::(1); + let (_, prompt_response_receiver) = tokio::sync::broadcast::channel::(1); + let helper = ChatHelper { + completer: ChatCompleter::new(prompt_request_sender, prompt_response_receiver), + hinter: ChatHinter::new(true), + validator: MultiLineValidator, + }; + + // Test profile with tangent mode - [dev] cyan, ↯ yellow, > magenta + let highlighted = helper.highlight_prompt("[dev] ↯ > ", true); + assert_eq!( + highlighted, + format!("{}{}{}", "[dev] ".cyan(), "↯ ".yellow(), "> ".magenta()) + ); + } + #[test] fn test_chat_hinter_command_hint() { let hinter = ChatHinter::new(true); diff --git a/crates/chat-cli/src/cli/chat/prompt_parser.rs b/crates/chat-cli/src/cli/chat/prompt_parser.rs index 832d8f9b4f..3969b4a5a7 100644 --- a/crates/chat-cli/src/cli/chat/prompt_parser.rs +++ b/crates/chat-cli/src/cli/chat/prompt_parser.rs @@ -5,40 +5,53 @@ use crate::cli::agent::DEFAULT_AGENT_NAME; pub struct PromptComponents { pub profile: Option, pub warning: bool, + pub tangent_mode: bool, } /// Parse prompt components from a plain text prompt pub fn parse_prompt_components(prompt: &str) -> Option { - // Expected format: "[profile] !> " or "> " or "!> " etc. + // Expected format: "[agent] !> " or "> " or "!> " or "[agent] ↯ > " or "↯ > " or "[agent] ↯ !> " etc. let mut profile = None; let mut warning = false; + let mut tangent_mode = false; let mut remaining = prompt.trim(); - // Check for profile pattern [profile] + // Check for agent pattern [agent] first if let Some(start) = remaining.find('[') { if let Some(end) = remaining.find(']') { if start < end { - profile = Some(remaining[start + 1..end].to_string()); + let content = &remaining[start + 1..end]; + profile = Some(content.to_string()); remaining = remaining[end + 1..].trim_start(); } } } - // Check for warning symbol ! + // Check for tangent mode ↯ first + if let Some(after_tangent) = remaining.strip_prefix('↯') { + tangent_mode = true; + remaining = after_tangent.trim_start(); + } + + // Check for warning symbol ! (comes after tangent mode) if remaining.starts_with('!') { warning = true; remaining = remaining[1..].trim_start(); } - // Should end with "> " + // Should end with "> " for both normal and tangent mode if remaining.trim_end() == ">" { - Some(PromptComponents { profile, warning }) + Some(PromptComponents { + profile, + warning, + tangent_mode, + }) } else { None } } -pub fn generate_prompt(current_profile: Option<&str>, warning: bool) -> String { +pub fn generate_prompt(current_profile: Option<&str>, warning: bool, tangent_mode: bool) -> String { // Generate plain text prompt that will be colored by highlight_prompt let warning_symbol = if warning { "!" } else { "" }; let profile_part = current_profile @@ -46,7 +59,11 @@ pub fn generate_prompt(current_profile: Option<&str>, warning: bool) -> String { .map(|p| format!("[{p}] ")) .unwrap_or_default(); - format!("{profile_part}{warning_symbol}> ") + if tangent_mode { + format!("{profile_part}↯ {warning_symbol}> ") + } else { + format!("{profile_part}{warning_symbol}> ") + } } #[cfg(test)] @@ -56,15 +73,26 @@ mod tests { #[test] fn test_generate_prompt() { // Test default prompt (no profile) - assert_eq!(generate_prompt(None, false), "> "); + assert_eq!(generate_prompt(None, false, false), "> "); // Test default prompt with warning - assert_eq!(generate_prompt(None, true), "!> "); + assert_eq!(generate_prompt(None, true, false), "!> "); + // Test tangent mode + assert_eq!(generate_prompt(None, false, true), "↯ > "); + // Test tangent mode with warning + assert_eq!(generate_prompt(None, true, true), "↯ !> "); // Test default profile (should be same as no profile) - assert_eq!(generate_prompt(Some(DEFAULT_AGENT_NAME), false), "> "); + assert_eq!(generate_prompt(Some(DEFAULT_AGENT_NAME), false, false), "> "); // Test custom profile - assert_eq!(generate_prompt(Some("test-profile"), false), "[test-profile] > "); + assert_eq!(generate_prompt(Some("test-profile"), false, false), "[test-profile] > "); + // Test custom profile with tangent mode + assert_eq!( + generate_prompt(Some("test-profile"), false, true), + "[test-profile] ↯ > " + ); // Test another custom profile with warning - assert_eq!(generate_prompt(Some("dev"), true), "[dev] !> "); + assert_eq!(generate_prompt(Some("dev"), true, false), "[dev] !> "); + // Test custom profile with warning and tangent mode + assert_eq!(generate_prompt(Some("dev"), true, true), "[dev] ↯ !> "); } #[test] @@ -73,21 +101,49 @@ mod tests { let components = parse_prompt_components("> ").unwrap(); assert!(components.profile.is_none()); assert!(!components.warning); + assert!(!components.tangent_mode); // Test warning prompt let components = parse_prompt_components("!> ").unwrap(); assert!(components.profile.is_none()); assert!(components.warning); + assert!(!components.tangent_mode); + + // Test tangent mode + let components = parse_prompt_components("↯ > ").unwrap(); + assert!(components.profile.is_none()); + assert!(!components.warning); + assert!(components.tangent_mode); + + // Test tangent mode with warning + let components = parse_prompt_components("↯ !> ").unwrap(); + assert!(components.profile.is_none()); + assert!(components.warning); + assert!(components.tangent_mode); // Test profile prompt let components = parse_prompt_components("[test] > ").unwrap(); assert_eq!(components.profile.as_deref(), Some("test")); assert!(!components.warning); + assert!(!components.tangent_mode); // Test profile with warning let components = parse_prompt_components("[dev] !> ").unwrap(); assert_eq!(components.profile.as_deref(), Some("dev")); assert!(components.warning); + assert!(!components.tangent_mode); + + // Test profile with tangent mode + let components = parse_prompt_components("[dev] ↯ > ").unwrap(); + assert_eq!(components.profile.as_deref(), Some("dev")); + assert!(!components.warning); + assert!(components.tangent_mode); + + // Test profile with warning and tangent mode + let components = parse_prompt_components("[dev] ↯ !> ").unwrap(); + assert_eq!(components.profile.as_deref(), Some("dev")); + assert!(components.warning); + assert!(components.tangent_mode); // Test invalid prompt assert!(parse_prompt_components("invalid").is_none()); diff --git a/crates/chat-cli/src/database/settings.rs b/crates/chat-cli/src/database/settings.rs index 87e2be2a53..1b3d57014a 100644 --- a/crates/chat-cli/src/database/settings.rs +++ b/crates/chat-cli/src/database/settings.rs @@ -2,16 +2,9 @@ use std::fmt::Display; use std::io::SeekFrom; use fd_lock::RwLock; -use serde_json::{ - Map, - Value, -}; +use serde_json::{Map, Value}; use tokio::fs::File; -use tokio::io::{ - AsyncReadExt, - AsyncSeekExt, - AsyncWriteExt, -}; +use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; use super::DatabaseError; @@ -29,6 +22,7 @@ pub enum Setting { KnowledgeChunkOverlap, KnowledgeIndexType, SkimCommandKey, + TangentModeKey, ChatGreetingEnabled, ApiTimeout, ChatEditMode, @@ -60,6 +54,7 @@ impl AsRef for Setting { Self::KnowledgeChunkOverlap => "knowledge.chunkOverlap", Self::KnowledgeIndexType => "knowledge.indexType", Self::SkimCommandKey => "chat.skimCommandKey", + Self::TangentModeKey => "chat.tangentModeKey", Self::ChatGreetingEnabled => "chat.greeting.enabled", Self::ApiTimeout => "api.timeout", Self::ChatEditMode => "chat.editMode", @@ -101,6 +96,7 @@ impl TryFrom<&str> for Setting { "knowledge.chunkOverlap" => Ok(Self::KnowledgeChunkOverlap), "knowledge.indexType" => Ok(Self::KnowledgeIndexType), "chat.skimCommandKey" => Ok(Self::SkimCommandKey), + "chat.tangentModeKey" => Ok(Self::TangentModeKey), "chat.greeting.enabled" => Ok(Self::ChatGreetingEnabled), "api.timeout" => Ok(Self::ApiTimeout), "chat.editMode" => Ok(Self::ChatEditMode), diff --git a/crates/chat-cli/src/telemetry/core.rs b/crates/chat-cli/src/telemetry/core.rs index a12947f8f8..a719687413 100644 --- a/crates/chat-cli/src/telemetry/core.rs +++ b/crates/chat-cli/src/telemetry/core.rs @@ -505,6 +505,8 @@ impl From for CodewhispererterminalChatConversationType { pub enum MessageMetaTag { /// A /compact request Compact, + /// A /tangent request + TangentMode, } /// Optional fields to add for a chatAddedMessage telemetry event. From 3f26b1f05d1b05106de21474c921ac3b15e62338 Mon Sep 17 00:00:00 2001 From: Kenneth Sanchez V Date: Thu, 21 Aug 2025 15:48:08 -0700 Subject: [PATCH 2/8] Apply cargo +nightly fmt formatting (#2664) Co-authored-by: Kenneth S. --- crates/chat-cli/src/cli/chat/cli/mod.rs | 12 +- crates/chat-cli/src/cli/chat/cli/tangent.rs | 15 +- crates/chat-cli/src/cli/chat/conversation.rs | 20 +- crates/chat-cli/src/cli/chat/mod.rs | 190 ++++++++++++------ crates/chat-cli/src/cli/chat/prompt.rs | 30 ++- crates/chat-cli/src/cli/chat/prompt_parser.rs | 3 +- crates/chat-cli/src/database/settings.rs | 11 +- 7 files changed, 205 insertions(+), 76 deletions(-) diff --git a/crates/chat-cli/src/cli/chat/cli/mod.rs b/crates/chat-cli/src/cli/chat/cli/mod.rs index 4bebac0593..c39eef446b 100644 --- a/crates/chat-cli/src/cli/chat/cli/mod.rs +++ b/crates/chat-cli/src/cli/chat/cli/mod.rs @@ -32,7 +32,12 @@ use tools::ToolsArgs; use crate::cli::chat::cli::subscribe::SubscribeArgs; use crate::cli::chat::cli::usage::UsageArgs; use crate::cli::chat::consts::AGENT_MIGRATION_DOC_URL; -use crate::cli::chat::{ChatError, ChatSession, ChatState, EXTRA_HELP}; +use crate::cli::chat::{ + ChatError, + ChatSession, + ChatState, + EXTRA_HELP, +}; use crate::cli::issue; use crate::os::Os; @@ -93,7 +98,10 @@ impl SlashCommand { Self::Clear(args) => args.execute(session).await, Self::Agent(subcommand) => subcommand.execute(os, session).await, Self::Profile => { - use crossterm::{execute, style}; + use crossterm::{ + execute, + style, + }; execute!( session.stderr, style::SetForegroundColor(style::Color::Yellow), diff --git a/crates/chat-cli/src/cli/chat/cli/tangent.rs b/crates/chat-cli/src/cli/chat/cli/tangent.rs index 5c1e32f325..f21ddf10b9 100644 --- a/crates/chat-cli/src/cli/chat/cli/tangent.rs +++ b/crates/chat-cli/src/cli/chat/cli/tangent.rs @@ -1,8 +1,15 @@ use clap::Args; use crossterm::execute; -use crossterm::style::{self, Color}; +use crossterm::style::{ + self, + Color, +}; -use crate::cli::chat::{ChatError, ChatSession, ChatState}; +use crate::cli::chat::{ + ChatError, + ChatSession, + ChatState, +}; use crate::os::Os; #[derive(Debug, PartialEq, Args)] @@ -52,9 +59,7 @@ impl TangentArgs { style::Print("/tangent"), style::SetForegroundColor(Color::DarkGrey), style::Print(" to restore the conversation later.\n"), - style::Print( - "Note: this functionality is experimental and may change or be removed in the future.\n" - ), + style::Print("Note: this functionality is experimental and may change or be removed in the future.\n"), style::SetForegroundColor(Color::Reset) )?; } diff --git a/crates/chat-cli/src/cli/chat/conversation.rs b/crates/chat-cli/src/cli/chat/conversation.rs index 06c998210c..55a1797800 100644 --- a/crates/chat-cli/src/cli/chat/conversation.rs +++ b/crates/chat-cli/src/cli/chat/conversation.rs @@ -1360,8 +1360,14 @@ mod tests { assert!(!conversation.is_in_tangent_mode()); // Add some main conversation history - conversation.set_next_user_message("main conversation".to_string()).await; - conversation.push_assistant_message(&mut os, AssistantMessage::new_response(None, "main response".to_string()), None); + conversation + .set_next_user_message("main conversation".to_string()) + .await; + conversation.push_assistant_message( + &mut os, + AssistantMessage::new_response(None, "main response".to_string()), + None, + ); conversation.transcript.push_back("main transcript".to_string()); let main_history_len = conversation.history.len(); @@ -1377,8 +1383,14 @@ mod tests { assert!(conversation.next_message.is_none()); // Add tangent conversation - conversation.set_next_user_message("tangent conversation".to_string()).await; - conversation.push_assistant_message(&mut os, AssistantMessage::new_response(None, "tangent response".to_string()), None); + conversation + .set_next_user_message("tangent conversation".to_string()) + .await; + conversation.push_assistant_message( + &mut os, + AssistantMessage::new_response(None, "tangent response".to_string()), + None, + ); // During tangent mode, history should have grown assert_eq!(conversation.history.len(), main_history_len + 1); diff --git a/crates/chat-cli/src/cli/chat/mod.rs b/crates/chat-cli/src/cli/chat/mod.rs index 5abaa31f07..17ce3bb14b 100644 --- a/crates/chat-cli/src/cli/chat/mod.rs +++ b/crates/chat-cli/src/cli/chat/mod.rs @@ -19,40 +19,109 @@ pub mod tool_manager; pub mod tools; pub mod util; use std::borrow::Cow; -use std::collections::{HashMap, VecDeque}; -use std::io::{IsTerminal, Read, Write}; +use std::collections::{ + HashMap, + VecDeque, +}; +use std::io::{ + IsTerminal, + Read, + Write, +}; use std::process::ExitCode; use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::{ + Duration, + Instant, +}; use amzn_codewhisperer_client::types::SubscriptionStatus; -use clap::{Args, CommandFactory, Parser}; +use clap::{ + Args, + CommandFactory, + Parser, +}; use cli::compact::CompactStrategy; -use cli::model::{get_available_models, select_model}; +use cli::model::{ + get_available_models, + select_model, +}; pub use conversation::ConversationState; use conversation::TokenWarningLevel; -use crossterm::style::{Attribute, Color, Stylize}; -use crossterm::{cursor, execute, queue, style, terminal}; -use tool_manager::{PromptQuery, PromptQueryResult}; -use eyre::{Report, Result, bail, eyre}; +use crossterm::style::{ + Attribute, + Color, + Stylize, +}; +use crossterm::{ + cursor, + execute, + queue, + style, + terminal, +}; +use eyre::{ + Report, + Result, + bail, + eyre, +}; use input_source::InputSource; -use message::{AssistantMessage, AssistantToolUse, ToolUseResult, ToolUseResultBlock}; -use parse::{ParseState, interpret_markdown}; -use parser::{RecvErrorKind, RequestMetadata, SendMessageStream}; +use message::{ + AssistantMessage, + AssistantToolUse, + ToolUseResult, + ToolUseResultBlock, +}; +use parse::{ + ParseState, + interpret_markdown, +}; +use parser::{ + RecvErrorKind, + RequestMetadata, + SendMessageStream, +}; use regex::Regex; -use spinners::{Spinner, Spinners}; +use spinners::{ + Spinner, + Spinners, +}; use thiserror::Error; use time::OffsetDateTime; use token_counter::TokenCounter; use tokio::signal::ctrl_c; -use tokio::sync::{Mutex, broadcast}; -use tool_manager::{ToolManager, ToolManagerBuilder}; +use tokio::sync::{ + Mutex, + broadcast, +}; +use tool_manager::{ + PromptQuery, + PromptQueryResult, + ToolManager, + ToolManagerBuilder, +}; use tools::gh_issue::GhIssueContext; -use tools::{NATIVE_TOOLS, OutputKind, QueuedTool, Tool, ToolSpec}; -use tracing::{debug, error, info, trace, warn}; +use tools::{ + NATIVE_TOOLS, + OutputKind, + QueuedTool, + Tool, + ToolSpec, +}; +use tracing::{ + debug, + error, + info, + trace, + warn, +}; use util::images::RichImageBlock; use util::ui::draw_box; -use util::{animate_output, play_notification_bell}; +use util::{ + animate_output, + play_notification_bell, +}; use winnow::Partial; use winnow::stream::Offset; @@ -61,22 +130,36 @@ use super::agent::{ PermissionEvalResult, }; use crate::api_client::model::ToolResultStatus; -use crate::api_client::{self, ApiClientError}; +use crate::api_client::{ + self, + ApiClientError, +}; use crate::auth::AuthError; use crate::auth::builder_id::is_idc_user; use crate::cli::agent::Agents; use crate::cli::chat::cli::SlashCommand; use crate::cli::chat::cli::model::find_model; -use crate::cli::chat::cli::prompts::{GetPromptError, PromptsSubcommand}; +use crate::cli::chat::cli::prompts::{ + GetPromptError, + PromptsSubcommand, +}; use crate::cli::chat::util::sanitize_unicode_tags; use crate::database::settings::Setting; use crate::mcp_client::Prompt; use crate::os::Os; use crate::telemetry::core::{ - AgentConfigInitArgs, ChatAddedMessageParams, ChatConversationType, MessageMetaTag, RecordUserTurnCompletionArgs, + AgentConfigInitArgs, + ChatAddedMessageParams, + ChatConversationType, + MessageMetaTag, + RecordUserTurnCompletionArgs, ToolUseEventBuilder, }; -use crate::telemetry::{ReasonCode, TelemetryResult, get_error_reason}; +use crate::telemetry::{ + ReasonCode, + TelemetryResult, + get_error_reason, +}; use crate::util::MCP_SERVER_TOOL_DELIMITER; const LIMIT_REACHED_TEXT: &str = color_print::cstr! { "You've used all your free requests for this month. You have two options: @@ -189,17 +272,13 @@ impl ChatArgs { agents.trust_all_tools = self.trust_all_tools; os.telemetry - .send_agent_config_init( - &os.database, - conversation_id.clone(), - AgentConfigInitArgs { - agents_loaded_count: md.load_count as i64, - agents_loaded_failed_count: md.load_failed_count as i64, - legacy_profile_migration_executed: md.migration_performed, - legacy_profile_migrated_count: md.migrated_count as i64, - launched_agent: md.launched_agent, - }, - ) + .send_agent_config_init(&os.database, conversation_id.clone(), AgentConfigInitArgs { + agents_loaded_count: md.load_count as i64, + agents_loaded_failed_count: md.load_failed_count as i64, + legacy_profile_migration_executed: md.migration_performed, + legacy_profile_migrated_count: md.migrated_count as i64, + launched_agent: md.launched_agent, + }) .await .map_err(|err| error!(?err, "failed to send agent config init telemetry")) .ok(); @@ -2693,31 +2772,26 @@ impl ChatSession { }; os.telemetry - .send_record_user_turn_completion( - &os.database, - conversation_id, - result, - RecordUserTurnCompletionArgs { - message_ids: mds.iter().map(|md| md.message_id.clone()).collect::<_>(), - request_ids: mds.iter().map(|md| md.request_id.clone()).collect::<_>(), - reason, - reason_desc, - status_code, - time_to_first_chunks_ms: mds - .iter() - .map(|md| md.time_to_first_chunk.map(|d| d.as_secs_f64() * 1000.0)) - .collect::<_>(), - chat_conversation_type: md.and_then(|md| md.chat_conversation_type), - assistant_response_length: mds.iter().map(|md| md.response_size as i64).sum(), - message_meta_tags: mds.last().map(|md| md.message_meta_tags.clone()).unwrap_or_default(), - user_prompt_length: mds.first().map(|md| md.user_prompt_length).unwrap_or_default() as i64, - user_turn_duration_seconds, - follow_up_count: mds - .iter() - .filter(|md| matches!(md.chat_conversation_type, Some(ChatConversationType::ToolUse))) - .count() as i64, - }, - ) + .send_record_user_turn_completion(&os.database, conversation_id, result, RecordUserTurnCompletionArgs { + message_ids: mds.iter().map(|md| md.message_id.clone()).collect::<_>(), + request_ids: mds.iter().map(|md| md.request_id.clone()).collect::<_>(), + reason, + reason_desc, + status_code, + time_to_first_chunks_ms: mds + .iter() + .map(|md| md.time_to_first_chunk.map(|d| d.as_secs_f64() * 1000.0)) + .collect::<_>(), + chat_conversation_type: md.and_then(|md| md.chat_conversation_type), + assistant_response_length: mds.iter().map(|md| md.response_size as i64).sum(), + message_meta_tags: mds.last().map(|md| md.message_meta_tags.clone()).unwrap_or_default(), + user_prompt_length: mds.first().map(|md| md.user_prompt_length).unwrap_or_default() as i64, + user_turn_duration_seconds, + follow_up_count: mds + .iter() + .filter(|md| matches!(md.chat_conversation_type, Some(ChatConversationType::ToolUse))) + .count() as i64, + }) .await .ok(); } diff --git a/crates/chat-cli/src/cli/chat/prompt.rs b/crates/chat-cli/src/cli/chat/prompt.rs index a2a50bddc9..ad93834dfc 100644 --- a/crates/chat-cli/src/cli/chat/prompt.rs +++ b/crates/chat-cli/src/cli/chat/prompt.rs @@ -2,14 +2,36 @@ use std::borrow::Cow; use std::cell::RefCell; use eyre::Result; -use rustyline::completion::{Completer, FilenameCompleter, extract_word}; +use rustyline::completion::{ + Completer, + FilenameCompleter, + extract_word, +}; use rustyline::error::ReadlineError; -use rustyline::highlight::{CmdKind, Highlighter}; +use rustyline::highlight::{ + CmdKind, + Highlighter, +}; use rustyline::hint::Hinter as RustylineHinter; use rustyline::history::DefaultHistory; -use rustyline::validate::{ValidationContext, ValidationResult, Validator}; +use rustyline::validate::{ + ValidationContext, + ValidationResult, + Validator, +}; use rustyline::{ - Cmd, Completer, CompletionType, Config, Context, EditMode, Editor, EventHandler, Helper, Hinter, KeyCode, KeyEvent, + Cmd, + Completer, + CompletionType, + Config, + Context, + EditMode, + Editor, + EventHandler, + Helper, + Hinter, + KeyCode, + KeyEvent, Modifiers, }; use winnow::stream::AsChar; diff --git a/crates/chat-cli/src/cli/chat/prompt_parser.rs b/crates/chat-cli/src/cli/chat/prompt_parser.rs index 3969b4a5a7..daf67a9859 100644 --- a/crates/chat-cli/src/cli/chat/prompt_parser.rs +++ b/crates/chat-cli/src/cli/chat/prompt_parser.rs @@ -10,7 +10,8 @@ pub struct PromptComponents { /// Parse prompt components from a plain text prompt pub fn parse_prompt_components(prompt: &str) -> Option { - // Expected format: "[agent] !> " or "> " or "!> " or "[agent] ↯ > " or "↯ > " or "[agent] ↯ !> " etc. + // Expected format: "[agent] !> " or "> " or "!> " or "[agent] ↯ > " or "↯ > " or "[agent] ↯ !> " + // etc. let mut profile = None; let mut warning = false; let mut tangent_mode = false; diff --git a/crates/chat-cli/src/database/settings.rs b/crates/chat-cli/src/database/settings.rs index 1b3d57014a..5243b9e648 100644 --- a/crates/chat-cli/src/database/settings.rs +++ b/crates/chat-cli/src/database/settings.rs @@ -2,9 +2,16 @@ use std::fmt::Display; use std::io::SeekFrom; use fd_lock::RwLock; -use serde_json::{Map, Value}; +use serde_json::{ + Map, + Value, +}; use tokio::fs::File; -use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; +use tokio::io::{ + AsyncReadExt, + AsyncSeekExt, + AsyncWriteExt, +}; use super::DatabaseError; From 8aed0e47fd40b465a3b84410aae7bf16fdb49de6 Mon Sep 17 00:00:00 2001 From: Kenneth Sanchez V Date: Mon, 25 Aug 2025 15:34:27 -0700 Subject: [PATCH 3/8] feat: implement agent-scoped knowledge base and context-specific search (#2647) --- crates/chat-cli/src/cli/chat/cli/knowledge.rs | 220 +++++++-------- crates/chat-cli/src/cli/chat/mod.rs | 7 +- .../chat-cli/src/cli/chat/tools/knowledge.rs | 10 +- crates/chat-cli/src/cli/chat/tools/mod.rs | 3 +- crates/chat-cli/src/cli/mod.rs | 4 + crates/chat-cli/src/util/directories.rs | 34 +++ crates/chat-cli/src/util/knowledge_store.rs | 266 +++++++++++------- .../src/client/async_implementation.rs | 38 +++ .../src/client/background/file_processor.rs | 3 +- .../src/client/context/context_manager.rs | 21 ++ docs/knowledge-management.md | 60 ++++ knowledge_beta_improvements_agents | 0 12 files changed, 442 insertions(+), 224 deletions(-) create mode 100644 knowledge_beta_improvements_agents diff --git a/crates/chat-cli/src/cli/chat/cli/knowledge.rs b/crates/chat-cli/src/cli/chat/cli/knowledge.rs index 4f31a787b2..d2b9e724d1 100644 --- a/crates/chat-cli/src/cli/chat/cli/knowledge.rs +++ b/crates/chat-cli/src/cli/chat/cli/knowledge.rs @@ -8,7 +8,6 @@ use crossterm::style::{ }; use eyre::Result; use semantic_search_client::{ - KnowledgeContext, OperationStatus, SystemStatus, }; @@ -103,6 +102,11 @@ impl KnowledgeSubcommand { } } + /// Get the current agent from the session + fn get_agent(session: &ChatSession) -> Option<&crate::cli::Agent> { + session.conversation.agents.get_active() + } + async fn execute_operation(&self, os: &Os, session: &mut ChatSession) -> OperationResult { match self { KnowledgeSubcommand::Show => { @@ -116,127 +120,105 @@ impl KnowledgeSubcommand { include, exclude, index_type, - } => Self::handle_add(os, path, include, exclude, index_type).await, - KnowledgeSubcommand::Remove { path } => Self::handle_remove(os, path).await, - KnowledgeSubcommand::Update { path } => Self::handle_update(os, path).await, + } => Self::handle_add(os, session, path, include, exclude, index_type).await, + KnowledgeSubcommand::Remove { path } => Self::handle_remove(os, session, path).await, + KnowledgeSubcommand::Update { path } => Self::handle_update(os, session, path).await, KnowledgeSubcommand::Clear => Self::handle_clear(os, session).await, - KnowledgeSubcommand::Status => Self::handle_status(os).await, - KnowledgeSubcommand::Cancel { operation_id } => Self::handle_cancel(os, operation_id.as_deref()).await, + KnowledgeSubcommand::Status => Self::handle_status(os, session).await, + KnowledgeSubcommand::Cancel { operation_id } => { + Self::handle_cancel(os, session, operation_id.as_deref()).await + }, } } async fn handle_show(os: &Os, session: &mut ChatSession) -> Result<(), std::io::Error> { - match KnowledgeStore::get_async_instance_with_os(os).await { - Ok(store) => { - let store = store.lock().await; - let entries = store.get_all().await.unwrap_or_else(|e| { - let _ = queue!( - session.stderr, - style::SetForegroundColor(Color::Red), - style::Print(&format!("Error getting knowledge base entries: {}\n", e)), - style::ResetColor - ); - Vec::new() - }); - let _ = Self::format_knowledge_entries(session, &entries); - }, - Err(e) => { - queue!( - session.stderr, - style::SetForegroundColor(Color::Red), - style::Print(&format!("Error accessing knowledge base: {}\n", e)), - style::SetForegroundColor(Color::Reset) - )?; - }, - } - Ok(()) - } + let agent_name = Self::get_agent(session).map(|a| a.name.clone()); - fn format_knowledge_entries( - session: &mut ChatSession, - knowledge_entries: &[KnowledgeContext], - ) -> Result<(), std::io::Error> { - if knowledge_entries.is_empty() { - queue!( - session.stderr, - style::Print("\nNo knowledge base entries found.\n"), - style::Print("💡 Tip: If indexing is in progress, entries may not appear until indexing completes.\n"), - style::Print(" Use 'knowledge status' to check active operations.\n\n") - )?; - } else { + // Show agent-specific knowledge + if let Some(agent) = agent_name { queue!( session.stderr, - style::Print("\n📚 Knowledge Base Entries:\n"), - style::Print(format!("{}\n", "━".repeat(80))) + style::SetAttribute(crossterm::style::Attribute::Bold), + style::SetForegroundColor(Color::Magenta), + style::Print(format!("👤 Agent ({}):\n", agent)), + style::SetAttribute(crossterm::style::Attribute::Reset), )?; - for entry in knowledge_entries { - Self::format_single_entry(session, &entry)?; - queue!(session.stderr, style::Print(format!("{}\n", "━".repeat(80))))?; + match KnowledgeStore::get_async_instance(os, Self::get_agent(session)).await { + Ok(store) => { + let store = store.lock().await; + let contexts = store.get_all().await.unwrap_or_default(); + + if contexts.is_empty() { + queue!( + session.stderr, + style::SetForegroundColor(Color::DarkGrey), + style::Print(" \n\n"), + style::SetForegroundColor(Color::Reset) + )?; + } else { + Self::format_knowledge_entries_with_indent(session, &contexts, " ")?; + } + }, + Err(_) => { + queue!( + session.stderr, + style::SetForegroundColor(Color::DarkGrey), + style::Print(" \n\n"), + style::SetForegroundColor(Color::Reset) + )?; + }, } - // Add final newline to match original formatting exactly - queue!(session.stderr, style::Print("\n"))?; } + Ok(()) } - fn format_single_entry(session: &mut ChatSession, entry: &&KnowledgeContext) -> Result<(), std::io::Error> { - queue!( - session.stderr, - style::SetAttribute(style::Attribute::Bold), - style::SetForegroundColor(Color::Cyan), - style::Print(format!("📂 {}: ", entry.id)), - style::SetForegroundColor(Color::Green), - style::Print(&entry.name), - style::SetAttribute(style::Attribute::Reset), - style::Print("\n") - )?; - - queue!( - session.stderr, - style::Print(format!(" Description: {}\n", entry.description)), - style::Print(format!( - " Created: {}\n", - entry.created_at.format("%Y-%m-%d %H:%M:%S") - )), - style::Print(format!( - " Updated: {}\n", - entry.updated_at.format("%Y-%m-%d %H:%M:%S") - )) - )?; - - if let Some(path) = &entry.source_path { - queue!(session.stderr, style::Print(format!(" Source: {}\n", path)))?; - } - - queue!( - session.stderr, - style::Print(" Items: "), - style::SetForegroundColor(Color::Yellow), - style::Print(entry.item_count.to_string()), - style::SetForegroundColor(Color::Reset), - style::Print(" | Index Type: "), - style::SetForegroundColor(Color::Magenta), - style::Print(entry.embedding_type.description().to_string()), - style::SetForegroundColor(Color::Reset), - style::Print(" | Persistent: ") - )?; - - if entry.persistent { + fn format_knowledge_entries_with_indent( + session: &mut ChatSession, + contexts: &[semantic_search_client::KnowledgeContext], + indent: &str, + ) -> Result<(), std::io::Error> { + for ctx in contexts { + // Main entry line with name and ID queue!( session.stderr, + style::Print(format!("{}📂 ", indent)), + style::SetAttribute(style::Attribute::Bold), + style::SetForegroundColor(Color::Grey), + style::Print(&ctx.name), style::SetForegroundColor(Color::Green), - style::Print("Yes"), + style::Print(format!(" ({})", &ctx.id[..8])), + style::SetAttribute(style::Attribute::Reset), style::SetForegroundColor(Color::Reset), style::Print("\n") )?; - } else { + + // Description line with original description queue!( session.stderr, - style::SetForegroundColor(Color::Yellow), - style::Print("No"), + style::Print(format!("{} ", indent)), + style::SetForegroundColor(Color::Grey), + style::Print(format!("{}\n", ctx.description)), + style::SetForegroundColor(Color::Reset) + )?; + + // Stats line with improved colors + queue!( + session.stderr, + style::Print(format!("{} ", indent)), + style::SetForegroundColor(Color::Green), + style::Print(format!("{} items", ctx.item_count)), + style::SetForegroundColor(Color::DarkGrey), + style::Print(" • "), + style::SetForegroundColor(Color::Blue), + style::Print(ctx.embedding_type.description()), + style::SetForegroundColor(Color::DarkGrey), + style::Print(" • "), + style::SetForegroundColor(Color::DarkGrey), + style::Print(format!("{}", ctx.updated_at.format("%m/%d %H:%M"))), style::SetForegroundColor(Color::Reset), - style::Print("\n") + style::Print("\n\n") )?; } Ok(()) @@ -254,6 +236,7 @@ impl KnowledgeSubcommand { async fn handle_add( os: &Os, + session: &mut ChatSession, path: &str, include_patterns: &[String], exclude_patterns: &[String], @@ -261,7 +244,9 @@ impl KnowledgeSubcommand { ) -> OperationResult { match Self::validate_and_sanitize_path(os, path) { Ok(sanitized_path) => { - let async_knowledge_store = match KnowledgeStore::get_async_instance_with_os(os).await { + let agent = Self::get_agent(session); + + let async_knowledge_store = match KnowledgeStore::get_async_instance(os, agent).await { Ok(store) => store, Err(e) => return OperationResult::Error(format!("Error accessing knowledge base: {}", e)), }; @@ -307,30 +292,40 @@ impl KnowledgeSubcommand { } /// Handle remove operation - async fn handle_remove(os: &Os, path: &str) -> OperationResult { + async fn handle_remove(os: &Os, session: &ChatSession, path: &str) -> OperationResult { let sanitized_path = sanitize_path_tool_arg(os, path); + let agent = Self::get_agent(session); - let async_knowledge_store = match KnowledgeStore::get_async_instance_with_os(os).await { + let async_knowledge_store = match KnowledgeStore::get_async_instance(os, agent).await { Ok(store) => store, Err(e) => return OperationResult::Error(format!("Error accessing knowledge base: {}", e)), }; let mut store = async_knowledge_store.lock().await; + let scope_desc = "agent"; + // Try path first, then name if store.remove_by_path(&sanitized_path.to_string_lossy()).await.is_ok() { - OperationResult::Success(format!("Removed knowledge base entry with path '{}'", path)) + OperationResult::Success(format!( + "Removed {} knowledge base entry with path '{}'", + scope_desc, path + )) } else if store.remove_by_name(path).await.is_ok() { - OperationResult::Success(format!("Removed knowledge base entry with name '{}'", path)) + OperationResult::Success(format!( + "Removed {} knowledge base entry with name '{}'", + scope_desc, path + )) } else { - OperationResult::Warning(format!("Entry not found in knowledge base: {}", path)) + OperationResult::Warning(format!("Entry not found in {} knowledge base: {}", scope_desc, path)) } } /// Handle update operation - async fn handle_update(os: &Os, path: &str) -> OperationResult { + async fn handle_update(os: &Os, session: &ChatSession, path: &str) -> OperationResult { match Self::validate_and_sanitize_path(os, path) { Ok(sanitized_path) => { - let async_knowledge_store = match KnowledgeStore::get_async_instance_with_os(os).await { + let agent = Self::get_agent(session); + let async_knowledge_store = match KnowledgeStore::get_async_instance(os, agent).await { Ok(store) => store, Err(e) => { return OperationResult::Error(format!("Error accessing knowledge base directory: {}", e)); @@ -368,7 +363,8 @@ impl KnowledgeSubcommand { return OperationResult::Info("Clear operation cancelled".to_string()); } - let async_knowledge_store = match KnowledgeStore::get_async_instance_with_os(os).await { + let agent = Self::get_agent(session); + let async_knowledge_store = match KnowledgeStore::get_async_instance(os, agent).await { Ok(store) => store, Err(e) => return OperationResult::Error(format!("Error accessing knowledge base directory: {}", e)), }; @@ -401,8 +397,9 @@ impl KnowledgeSubcommand { } /// Handle status operation - async fn handle_status(os: &Os) -> OperationResult { - let async_knowledge_store = match KnowledgeStore::get_async_instance_with_os(os).await { + async fn handle_status(os: &Os, session: &ChatSession) -> OperationResult { + let agent = Self::get_agent(session); + let async_knowledge_store = match KnowledgeStore::get_async_instance(os, agent).await { Ok(store) => store, Err(e) => return OperationResult::Error(format!("Error accessing knowledge base directory: {}", e)), }; @@ -512,8 +509,9 @@ impl KnowledgeSubcommand { } /// Handle cancel operation - async fn handle_cancel(os: &Os, operation_id: Option<&str>) -> OperationResult { - let async_knowledge_store = match KnowledgeStore::get_async_instance_with_os(os).await { + async fn handle_cancel(os: &Os, session: &ChatSession, operation_id: Option<&str>) -> OperationResult { + let agent = Self::get_agent(session); + let async_knowledge_store = match KnowledgeStore::get_async_instance(os, agent).await { Ok(store) => store, Err(e) => return OperationResult::Error(format!("Error accessing knowledge base directory: {}", e)), }; diff --git a/crates/chat-cli/src/cli/chat/mod.rs b/crates/chat-cli/src/cli/chat/mod.rs index 17ce3bb14b..1d668cb331 100644 --- a/crates/chat-cli/src/cli/chat/mod.rs +++ b/crates/chat-cli/src/cli/chat/mod.rs @@ -1950,7 +1950,12 @@ impl ChatSession { let invoke_result = tool .tool - .invoke(os, &mut self.stdout, &mut self.conversation.file_line_tracker) + .invoke( + os, + &mut self.stdout, + &mut self.conversation.file_line_tracker, + self.conversation.agents.get_active(), + ) .await; if self.spinner.is_some() { diff --git a/crates/chat-cli/src/cli/chat/tools/knowledge.rs b/crates/chat-cli/src/cli/chat/tools/knowledge.rs index 9f85c1b01c..7bde4f0651 100644 --- a/crates/chat-cli/src/cli/chat/tools/knowledge.rs +++ b/crates/chat-cli/src/cli/chat/tools/knowledge.rs @@ -312,9 +312,13 @@ impl Knowledge { Ok(()) } - pub async fn invoke(&self, os: &Os, _updates: &mut impl Write) -> Result { - // Get the async knowledge store singleton with OS-aware directory - let async_knowledge_store = KnowledgeStore::get_async_instance_with_os(os) + pub async fn invoke( + &self, + os: &Os, + _updates: &mut impl Write, + agent: Option<&crate::cli::Agent>, + ) -> Result { + let async_knowledge_store = KnowledgeStore::get_async_instance(os, agent) .await .map_err(|e| eyre::eyre!("Failed to access knowledge base: {}", e))?; let mut store = async_knowledge_store.lock().await; diff --git a/crates/chat-cli/src/cli/chat/tools/mod.rs b/crates/chat-cli/src/cli/chat/tools/mod.rs index ae7a30900f..9fc66b0ef8 100644 --- a/crates/chat-cli/src/cli/chat/tools/mod.rs +++ b/crates/chat-cli/src/cli/chat/tools/mod.rs @@ -120,6 +120,7 @@ impl Tool { os: &Os, stdout: &mut impl Write, line_tracker: &mut HashMap, + agent: Option<&crate::cli::agent::Agent>, ) -> Result { match self { Tool::FsRead(fs_read) => fs_read.invoke(os, stdout).await, @@ -128,7 +129,7 @@ impl Tool { Tool::UseAws(use_aws) => use_aws.invoke(os, stdout).await, Tool::Custom(custom_tool) => custom_tool.invoke(os, stdout).await, Tool::GhIssue(gh_issue) => gh_issue.invoke(os, stdout).await, - Tool::Knowledge(knowledge) => knowledge.invoke(os, stdout).await, + Tool::Knowledge(knowledge) => knowledge.invoke(os, stdout, agent).await, Tool::Thinking(think) => think.invoke(stdout).await, } } diff --git a/crates/chat-cli/src/cli/mod.rs b/crates/chat-cli/src/cli/mod.rs index 33238b9da3..1b89e0f8b9 100644 --- a/crates/chat-cli/src/cli/mod.rs +++ b/crates/chat-cli/src/cli/mod.rs @@ -16,6 +16,10 @@ use std::io::{ use std::process::ExitCode; use agent::AgentArgs; +pub use agent::{ + Agent, + DEFAULT_AGENT_NAME, +}; use anstream::println; pub use chat::ConversationState; use clap::{ diff --git a/crates/chat-cli/src/util/directories.rs b/crates/chat-cli/src/util/directories.rs index 64006e467a..50091ce87c 100644 --- a/crates/chat-cli/src/util/directories.rs +++ b/crates/chat-cli/src/util/directories.rs @@ -10,6 +10,7 @@ use globset::{ }; use thiserror::Error; +use crate::cli::DEFAULT_AGENT_NAME; use crate::os::Os; #[derive(Debug, Error)] @@ -220,6 +221,39 @@ pub fn knowledge_bases_dir(os: &Os) -> Result { Ok(home_dir(os)?.join(".aws").join("amazonq").join("knowledge_bases")) } +/// The directory for agent-specific knowledge base storage +pub fn agent_knowledge_dir(os: &Os, agent: Option<&crate::cli::Agent>) -> Result { + let unique_id = if let Some(agent) = agent { + generate_agent_unique_id(agent) + } else { + // Default agent case + DEFAULT_AGENT_NAME.to_string() + }; + Ok(knowledge_bases_dir(os)?.join(unique_id)) +} + +/// Generate a unique identifier for an agent based on its path and name +fn generate_agent_unique_id(agent: &crate::cli::Agent) -> String { + use std::collections::hash_map::DefaultHasher; + use std::hash::{ + Hash, + Hasher, + }; + + if let Some(path) = &agent.path { + // Create a hash from the agent's path for uniqueness + let mut hasher = DefaultHasher::new(); + path.hash(&mut hasher); + let path_hash = hasher.finish(); + + // Combine hash with agent name for readability + format!("{}_{:x}", agent.name, path_hash) + } else { + // For agents without a path (like default), use just the name + agent.name.clone() + } +} + /// The path to the fig settings file pub fn settings_path() -> Result { Ok(fig_data_dir()?.join("settings.json")) diff --git a/crates/chat-cli/src/util/knowledge_store.rs b/crates/chat-cli/src/util/knowledge_store.rs index 559429c9d5..2cf1340443 100644 --- a/crates/chat-cli/src/util/knowledge_store.rs +++ b/crates/chat-cli/src/util/knowledge_store.rs @@ -13,9 +13,9 @@ use semantic_search_client::types::{ SearchResult, }; use tokio::sync::Mutex; -use tracing::debug; use uuid::Uuid; +use crate::cli::DEFAULT_AGENT_NAME; use crate::os::Os; use crate::util::directories; @@ -91,91 +91,120 @@ impl AddOptions { #[derive(Debug)] pub enum KnowledgeError { - ClientError(String), + SearchError(String), } impl std::fmt::Display for KnowledgeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - KnowledgeError::ClientError(msg) => write!(f, "Client error: {}", msg), + KnowledgeError::SearchError(msg) => write!(f, "Search error: {}", msg), } } } impl std::error::Error for KnowledgeError {} -/// Async knowledge store - just a thin wrapper! +/// Async knowledge store - manages agent specific knowledge bases pub struct KnowledgeStore { - client: AsyncSemanticSearchClient, + agent_client: AsyncSemanticSearchClient, + agent_dir: PathBuf, } impl KnowledgeStore { - /// Get singleton instance with directory from OS (includes migration) - pub async fn get_async_instance_with_os(os: &Os) -> Result>, directories::DirectoryError> { - let knowledge_dir = crate::util::directories::knowledge_bases_dir(os)?; - Self::migrate_legacy_knowledge_base(&knowledge_dir).await; - Ok(Self::get_async_instance_with_os_settings(os, knowledge_dir).await) + /// Get singleton instance with optional agent + pub async fn get_async_instance( + os: &Os, + agent: Option<&crate::cli::Agent>, + ) -> Result>, directories::DirectoryError> { + static ASYNC_INSTANCE: Lazy>>>> = + Lazy::new(|| tokio::sync::Mutex::new(None)); + + if cfg!(test) { + // For tests, create a new instance each time + let store = Self::new_with_os_settings(os, agent) + .await + .map_err(|_e| directories::DirectoryError::Io(std::io::Error::other("Failed to create store")))?; + Ok(Arc::new(Mutex::new(store))) + } else { + let current_agent_dir = crate::util::directories::agent_knowledge_dir(os, agent)?; + + let mut instance_guard = ASYNC_INSTANCE.lock().await; + + let needs_reinit = match instance_guard.as_ref() { + None => true, + Some(store) => { + let store_guard = store.lock().await; + store_guard.agent_dir != current_agent_dir + }, + }; + + if needs_reinit { + // Check for migration before initializing the client + Self::migrate_legacy_knowledge_base(¤t_agent_dir).await; + + let store = Self::new_with_os_settings(os, agent) + .await + .map_err(|_e| directories::DirectoryError::Io(std::io::Error::other("Failed to create store")))?; + *instance_guard = Some(Arc::new(Mutex::new(store))); + } + + Ok(instance_guard.as_ref().unwrap().clone()) + } } /// Migrate legacy knowledge base from old location if needed - async fn migrate_legacy_knowledge_base(knowledge_dir: &PathBuf) { + async fn migrate_legacy_knowledge_base(agent_dir: &PathBuf) -> bool { + let mut migrated = false; + + // Extract agent identifier from the directory path (last component) + let current_agent_id = agent_dir + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(DEFAULT_AGENT_NAME); + + // Migrate from legacy ~/.semantic_search let old_flat_dir = dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) .join(".semantic_search"); - if old_flat_dir.exists() && !knowledge_dir.exists() { - // Create parent directories first - if let Some(parent) = knowledge_dir.parent() { - if let Err(e) = std::fs::create_dir_all(parent) { - debug!( - "Warning: Failed to create parent directories for knowledge base migration: {}", - e - ); - return; - } + if old_flat_dir.exists() && !agent_dir.exists() { + if let Some(parent) = agent_dir.parent() { + std::fs::create_dir_all(parent).ok(); } - - // Attempt migration - if let Err(e) = std::fs::rename(&old_flat_dir, knowledge_dir) { - debug!( - "Warning: Failed to migrate legacy knowledge base from {} to {}: {}", - old_flat_dir.display(), - knowledge_dir.display(), - e - ); - } else { + if std::fs::rename(&old_flat_dir, agent_dir).is_ok() { println!( "✅ Migrated knowledge base from {} to {}", old_flat_dir.display(), - knowledge_dir.display() + agent_dir.display() ); + return true; } } - } - - /// Get singleton instance with OS settings (primary method) - pub async fn get_async_instance_with_os_settings(os: &crate::os::Os, base_dir: PathBuf) -> Arc> { - static ASYNC_INSTANCE: Lazy>>> = - Lazy::new(tokio::sync::OnceCell::new); - if cfg!(test) { - Arc::new(Mutex::new( - KnowledgeStore::new_with_os_settings(os, base_dir) - .await - .expect("Failed to create test async knowledge store"), - )) - } else { - ASYNC_INSTANCE - .get_or_init(|| async { - Arc::new(Mutex::new( - KnowledgeStore::new_with_os_settings(os, base_dir) - .await - .expect("Failed to create async knowledge store"), - )) - }) - .await - .clone() + // Migrate from knowledge_bases root - get file list first to avoid recursion + if let Some(kb_root) = agent_dir.parent() { + if kb_root.exists() { + if let Ok(entries) = std::fs::read_dir(kb_root) { + let files_to_migrate: Vec<_> = entries + .flatten() + .filter(|entry| { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + name_str != current_agent_id && name_str != DEFAULT_AGENT_NAME && !name_str.starts_with('.') + }) + .collect(); + + std::fs::create_dir_all(agent_dir).ok(); + for entry in files_to_migrate { + let dst_path = agent_dir.join(entry.file_name()); + if !dst_path.exists() && std::fs::rename(entry.path(), &dst_path).is_ok() { + migrated = true; + } + } + } + } } + migrated } /// Create SemanticSearchConfig from database settings with fallbacks to defaults @@ -227,13 +256,18 @@ impl KnowledgeStore { } /// Create instance with database settings from OS - pub async fn new_with_os_settings(os: &crate::os::Os, base_dir: PathBuf) -> Result { - let config = Self::create_config_from_db_settings(os, base_dir.clone()); - let client = AsyncSemanticSearchClient::with_config(&base_dir, config) + async fn new_with_os_settings(os: &crate::os::Os, agent: Option<&crate::cli::Agent>) -> Result { + let agent_dir = crate::util::directories::agent_knowledge_dir(os, agent)?; + let agent_config = Self::create_config_from_db_settings(os, agent_dir.clone()); + let agent_client = AsyncSemanticSearchClient::with_config(&agent_dir, agent_config) .await - .map_err(|e| eyre::eyre!("Failed to create client: {}", e))?; + .map_err(|e| eyre::eyre!("Failed to create agent client at {}: {}", agent_dir.display(), e))?; - Ok(Self { client }) + let store = Self { + agent_client, + agent_dir, + }; + Ok(store) } /// Add context with flexible options @@ -286,7 +320,7 @@ impl KnowledgeStore { }, }; - match self.client.add_context(request).await { + match self.agent_client.add_context(request).await { Ok((operation_id, _)) => { let mut message = format!( "🚀 Started indexing '{}'\n📁 Path: {}\n🆔 Operation ID: {}", @@ -317,63 +351,81 @@ impl KnowledgeStore { } } - pub async fn get_all(&self) -> Result, KnowledgeError> { - Ok(self.client.get_contexts().await) + /// Get all contexts from agent client + pub async fn get_all(&self) -> Result, String> { + Ok(self.agent_client.get_contexts().await) } /// Search - delegates to async client - pub async fn search(&self, query: &str, _context_id: Option<&str>) -> Result, KnowledgeError> { - let results = self - .client - .search_all(query, None) - .await - .map_err(|e| KnowledgeError::ClientError(e.to_string()))?; + pub async fn search(&self, query: &str, context_id: Option<&str>) -> Result, KnowledgeError> { + if let Some(context_id) = context_id { + // Search specific context + let results = self + .agent_client + .search_context(context_id, query, None) + .await + .map_err(|e| KnowledgeError::SearchError(e.to_string()))?; + Ok(results) + } else { + // Search all contexts + let mut flattened = Vec::new(); - let mut flattened = Vec::new(); - for (_, context_results) in results { - flattened.extend(context_results); - } + let agent_results = self + .agent_client + .search_all(query, None) + .await + .map_err(|e| KnowledgeError::SearchError(e.to_string()))?; - flattened.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap_or(std::cmp::Ordering::Equal)); + for (_, context_results) in agent_results { + flattened.extend(context_results); + } + + flattened.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap_or(std::cmp::Ordering::Equal)); - Ok(flattened) + Ok(flattened) + } } - /// Get status data - delegates to async client + /// Get status data pub async fn get_status_data(&self) -> Result { - self.client - .get_status_data() - .await - .map_err(|e| format!("Failed to get status data: {}", e)) + self.agent_client.get_status_data().await.map_err(|e| e.to_string()) } - /// Cancel operation - delegates to async client + /// Cancel active operation. + /// last operation if no operation id is provided. pub async fn cancel_operation(&mut self, operation_id: Option<&str>) -> Result { if let Some(short_id) = operation_id { - let available_ops = self.client.list_operation_ids().await; + let available_ops = self.agent_client.list_operation_ids().await; if available_ops.is_empty() { - // This is fine. - return Ok("No operations to cancel".to_string()); + return Ok("No active operations to cancel".to_string()); } // Try to parse as full UUID first if let Ok(uuid) = Uuid::parse_str(short_id) { - self.client.cancel_operation(uuid).await.map_err(|e| e.to_string()) + self.agent_client + .cancel_operation(uuid) + .await + .map_err(|e| e.to_string()) } else { // Try to find by short ID (first 8 characters) - if let Some(full_uuid) = self.client.find_operation_by_short_id(short_id).await { - self.client.cancel_operation(full_uuid).await.map_err(|e| e.to_string()) + if let Some(full_uuid) = self.agent_client.find_operation_by_short_id(short_id).await { + self.agent_client + .cancel_operation(full_uuid) + .await + .map_err(|e| e.to_string()) } else { + let available_ops_str: Vec = + available_ops.iter().map(|id| id.clone()[..8].to_string()).collect(); Err(format!( - "No operation found matching ID: {}\nAvailable operations:\n{}", + "Operation '{}' not found. Available operations: {}", short_id, - available_ops.join("\n") + available_ops_str.join(", ") )) } } } else { - // Cancel most recent operation (not all operations) - self.client + // Cancel most recent operation + self.agent_client .cancel_most_recent_operation() .await .map_err(|e| e.to_string()) @@ -382,7 +434,7 @@ impl KnowledgeStore { /// Clear all contexts (background operation) pub async fn clear(&mut self) -> Result { - match self.client.clear_all().await { + match self.agent_client.clear_all().await { Ok((operation_id, _cancel_token)) => Ok(format!( "🚀 Started clearing all contexts in background.\n📊 Use 'knowledge status' to check progress.\n🆔 Operation ID: {}", &operation_id.to_string()[..8] @@ -393,7 +445,7 @@ impl KnowledgeStore { /// Clear all contexts immediately (synchronous operation) pub async fn clear_immediate(&mut self) -> Result { - match self.client.clear_all_immediate().await { + match self.agent_client.clear_all_immediate().await { Ok(count) => Ok(format!("✅ Successfully cleared {} knowledge base entries", count)), Err(e) => Err(format!("Failed to clear knowledge base: {}", e)), } @@ -401,8 +453,8 @@ impl KnowledgeStore { /// Remove context by path pub async fn remove_by_path(&mut self, path: &str) -> Result<(), String> { - if let Some(context) = self.client.get_context_by_path(path).await { - self.client + if let Some(context) = self.agent_client.get_context_by_path(path).await { + self.agent_client .remove_context_by_id(&context.id) .await .map_err(|e| e.to_string()) @@ -413,8 +465,8 @@ impl KnowledgeStore { /// Remove context by name pub async fn remove_by_name(&mut self, name: &str) -> Result<(), String> { - if let Some(context) = self.client.get_context_by_name(name).await { - self.client + if let Some(context) = self.agent_client.get_context_by_name(name).await { + self.agent_client .remove_context_by_id(&context.id) .await .map_err(|e| e.to_string()) @@ -425,7 +477,7 @@ impl KnowledgeStore { /// Remove context by ID pub async fn remove_by_id(&mut self, context_id: &str) -> Result<(), String> { - self.client + self.agent_client .remove_context_by_id(context_id) .await .map_err(|e| e.to_string()) @@ -433,14 +485,14 @@ impl KnowledgeStore { /// Update context by path pub async fn update_by_path(&mut self, path_str: &str) -> Result { - if let Some(context) = self.client.get_context_by_path(path_str).await { + if let Some(context) = self.agent_client.get_context_by_path(path_str).await { // Remove the existing context first - self.client + self.agent_client .remove_context_by_id(&context.id) .await .map_err(|e| e.to_string())?; - // Then add it back with the same name and original patterns + // Then add it back with the same name and original patterns (agent scope) let options = AddOptions { description: None, include_patterns: context.include_patterns.clone(), @@ -450,7 +502,7 @@ impl KnowledgeStore { self.add(&context.name, path_str, options).await } else { // Debug: List all available contexts - let available_paths = self.client.list_context_paths().await; + let available_paths = self.agent_client.list_context_paths().await; if available_paths.is_empty() { Err("No contexts found. Add a context first with 'knowledge add '".to_string()) } else { @@ -465,7 +517,7 @@ impl KnowledgeStore { /// Update context by ID pub async fn update_context_by_id(&mut self, context_id: &str, path_str: &str) -> Result { - let contexts = self.get_all().await.map_err(|e| e.to_string())?; + let contexts = self.get_all().await.map_err(|e| e.clone())?; let context = contexts .iter() .find(|c| c.id == context_id) @@ -474,7 +526,7 @@ impl KnowledgeStore { let context_name = context.name.clone(); // Remove the existing context first - self.client + self.agent_client .remove_context_by_id(context_id) .await .map_err(|e| e.to_string())?; @@ -491,14 +543,14 @@ impl KnowledgeStore { /// Update context by name pub async fn update_context_by_name(&mut self, name: &str, path_str: &str) -> Result { - if let Some(context) = self.client.get_context_by_name(name).await { + if let Some(context) = self.agent_client.get_context_by_name(name).await { // Remove the existing context first - self.client + self.agent_client .remove_context_by_id(&context.id) .await .map_err(|e| e.to_string())?; - // Then add it back with the same name and original patterns + // Then add it back with the same name and original patterns (agent scope) let options = AddOptions { description: None, include_patterns: context.include_patterns.clone(), diff --git a/crates/semantic-search-client/src/client/async_implementation.rs b/crates/semantic-search-client/src/client/async_implementation.rs index ceb1f4c192..98b5f9571d 100644 --- a/crates/semantic-search-client/src/client/async_implementation.rs +++ b/crates/semantic-search-client/src/client/async_implementation.rs @@ -419,6 +419,44 @@ impl AsyncSemanticSearchClient { .await } + /// Search in a specific context + /// + /// # Arguments + /// + /// * `context_id` - ID of the context to search in + /// * `query_text` - Search query + /// * `result_limit` - Maximum number of results to return (if None, uses default_results from + /// config) + /// + /// # Returns + /// + /// A vector of search results + pub async fn search_context( + &self, + context_id: &str, + query_text: &str, + result_limit: Option, + ) -> Result { + if context_id.is_empty() { + return Err(SemanticSearchError::InvalidArgument( + "Context ID cannot be empty".to_string(), + )); + } + + if query_text.is_empty() { + return Err(SemanticSearchError::InvalidArgument( + "Query text cannot be empty".to_string(), + )); + } + + let effective_limit = result_limit.unwrap_or(self.config.default_results); + + self.context_manager + .search_context(context_id, query_text, effective_limit, &*self.embedder) + .await? + .ok_or_else(|| SemanticSearchError::ContextNotFound(context_id.to_string())) + } + /// Cancels a running background operation. /// /// This method attempts to cancel an operation identified by its UUID. diff --git a/crates/semantic-search-client/src/client/background/file_processor.rs b/crates/semantic-search-client/src/client/background/file_processor.rs index 495234f660..4e2ef935c7 100644 --- a/crates/semantic-search-client/src/client/background/file_processor.rs +++ b/crates/semantic-search-client/src/client/background/file_processor.rs @@ -33,6 +33,7 @@ impl FileProcessor { let dir_path = dir_path.to_path_buf(); let active_operations = operation_manager.get_active_operations_ref().clone(); let pattern_filter = Self::create_pattern_filter(include_patterns, exclude_patterns)?; + let max_files = self.config.max_files; let count_result = tokio::task::spawn_blocking(move || { let mut count = 0; @@ -73,7 +74,7 @@ impl FileProcessor { } } - if count > 5000 { + if count > max_files { break; } } diff --git a/crates/semantic-search-client/src/client/context/context_manager.rs b/crates/semantic-search-client/src/client/context/context_manager.rs index 5910be6cb7..9df042c021 100644 --- a/crates/semantic-search-client/src/client/context/context_manager.rs +++ b/crates/semantic-search-client/src/client/context/context_manager.rs @@ -107,6 +107,27 @@ impl ContextManager { Ok(all_results) } + /// Search in a specific context + pub async fn search_context( + &self, + context_id: &str, + query_text: &str, + effective_limit: usize, + embedder: &dyn TextEmbedderTrait, + ) -> Result> { + let contexts_metadata = self.contexts.read().await; + let context_meta = contexts_metadata + .get(context_id) + .ok_or_else(|| SemanticSearchError::ContextNotFound(context_id.to_string()))?; + + if context_meta.embedding_type.is_bm25() { + Ok(self.search_bm25_context(context_id, query_text, effective_limit).await) + } else { + self.search_semantic_context(context_id, query_text, effective_limit, embedder) + .await + } + } + async fn search_bm25_context(&self, context_id: &str, query_text: &str, limit: usize) -> Option { let bm25_contexts = tokio::time::timeout(Duration::from_millis(100), self.bm25_contexts.read()) .await diff --git a/docs/knowledge-management.md b/docs/knowledge-management.md index efce4da4be..a403092d4b 100644 --- a/docs/knowledge-management.md +++ b/docs/knowledge-management.md @@ -166,6 +166,66 @@ Configure knowledge base behavior: `q settings knowledge.defaultIncludePatterns '["**/*.rs", "**/*.md"]'` # Default include patterns `q settings knowledge.defaultExcludePatterns '["target/**", "node_modules/**"]'` # Default exclude patterns +## Agent-Specific Knowledge Bases + +### Isolated Knowledge Storage + +Each agent maintains its own isolated knowledge base, ensuring that knowledge contexts are scoped to the specific agent you're working with. This provides better organization and prevents knowledge conflicts between different agents. + +### Folder Structure + +Knowledge bases are stored in the following directory structure: + +``` +~/.q/knowledge_bases/ +├── q_cli_default/ # Default agent knowledge base +│ ├── contexts.json # Metadata for all contexts +│ ├── context-id-1/ # Individual context storage +│ │ ├── data.json # Semantic search data +│ │ └── bm25_data.json # BM25 search data (if using Fast index) +│ └── context-id-2/ +│ ├── data.json +│ └── bm25_data.json +├── my-custom-agent/ # Custom agent knowledge base +│ ├── contexts.json +│ ├── context-id-3/ +│ │ └── data.json +│ └── context-id-4/ +│ └── data.json +└── another-agent/ # Another agent's knowledge base + ├── contexts.json + └── context-id-5/ + └── data.json +``` + +### How Agent Isolation Works + +- **Automatic Scoping**: When you use `/knowledge` commands, they automatically operate on the current agent's knowledge base +- **No Cross-Agent Access**: Agent A cannot access or search knowledge contexts created by Agent B +- **Independent Configuration**: Each agent can have different knowledge base settings and contexts +- **Migration Support**: Legacy knowledge bases are automatically migrated to the default agent on first use + +### Agent Switching + +When you switch between agents, your knowledge commands will automatically work with that agent's specific knowledge base: + +```bash +# Working with default agent +/knowledge add /path/to/docs + +# Switch to custom agent +q chat --agent my-custom-agent + +# This creates a separate knowledge base for my-custom-agent +/knowledge add /path/to/agent/docs + +# Switch back to default +q chat + +# Only sees the original project-docs, not agent-specific-docs +/knowledge show +``` + ## How It Works #### Indexing Process diff --git a/knowledge_beta_improvements_agents b/knowledge_beta_improvements_agents new file mode 100644 index 0000000000..e69de29bb2 From aa84de05d54b4dc0d68154ce5951ad582c6cb5c0 Mon Sep 17 00:00:00 2001 From: kkashilk <93673379+kkashilk@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:07:14 -0700 Subject: [PATCH 4/8] Short-Term fix for SendTelemetry API Validation errors (#2694) --- crates/chat-cli/src/telemetry/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/chat-cli/src/telemetry/mod.rs b/crates/chat-cli/src/telemetry/mod.rs index ccfdea22b5..68e5c78d16 100644 --- a/crates/chat-cli/src/telemetry/mod.rs +++ b/crates/chat-cli/src/telemetry/mod.rs @@ -572,12 +572,17 @@ impl TelemetryClient { } = &event.ty { let user_context = self.user_context().unwrap(); + // Short-Term fix for Validation errors - + // chatAddMessageEvent.timeBetweenChunks' : Member must have length less than or equal to 100 + let time_between_chunks_truncated = time_between_chunks_ms + .as_ref() + .map(|chunks| chunks.iter().take(100).cloned().collect()); let chat_add_message_event = match ChatAddMessageEvent::builder() .conversation_id(conversation_id) .message_id(message_id.clone().unwrap_or("not_set".to_string())) .set_time_to_first_chunk_milliseconds(*time_to_first_chunk_ms) - .set_time_between_chunks(time_between_chunks_ms.clone()) + .set_time_between_chunks(time_between_chunks_truncated) .set_response_length(*assistant_response_length) .build() { From d575b0df98ecdc9229cfea94256f0d9778281190 Mon Sep 17 00:00:00 2001 From: abhraina-aws Date: Tue, 26 Aug 2025 11:12:03 -0700 Subject: [PATCH 5/8] feat: add introspect tool for Q CLI self-awareness (#2677) - Add introspect tool with comprehensive Q CLI documentation - Include auto-tangent mode for isolated introspect conversations - Add GitHub links for documentation references - Support agent file locations and built-in tools documentation - Add automatic settings documentation with native enum descriptions - Use strum EnumMessage for maintainable setting descriptions --- crates/chat-cli/src/cli/chat/mod.rs | 212 +++++++----------- crates/chat-cli/src/cli/chat/tool_manager.rs | 114 +++------- .../chat-cli/src/cli/chat/tools/introspect.rs | 132 +++++++++++ crates/chat-cli/src/cli/chat/tools/mod.rs | 39 ++-- .../src/cli/chat/tools/tool_index.json | 14 ++ crates/chat-cli/src/database/settings.rs | 44 +++- 6 files changed, 303 insertions(+), 252 deletions(-) create mode 100644 crates/chat-cli/src/cli/chat/tools/introspect.rs diff --git a/crates/chat-cli/src/cli/chat/mod.rs b/crates/chat-cli/src/cli/chat/mod.rs index 1d668cb331..31489d10bf 100644 --- a/crates/chat-cli/src/cli/chat/mod.rs +++ b/crates/chat-cli/src/cli/chat/mod.rs @@ -19,147 +19,60 @@ pub mod tool_manager; pub mod tools; pub mod util; use std::borrow::Cow; -use std::collections::{ - HashMap, - VecDeque, -}; -use std::io::{ - IsTerminal, - Read, - Write, -}; +use std::collections::{HashMap, VecDeque}; +use std::io::{IsTerminal, Read, Write}; use std::process::ExitCode; use std::sync::Arc; -use std::time::{ - Duration, - Instant, -}; +use std::time::{Duration, Instant}; use amzn_codewhisperer_client::types::SubscriptionStatus; -use clap::{ - Args, - CommandFactory, - Parser, -}; +use clap::{Args, CommandFactory, Parser}; use cli::compact::CompactStrategy; -use cli::model::{ - get_available_models, - select_model, -}; +use cli::model::{get_available_models, select_model}; pub use conversation::ConversationState; use conversation::TokenWarningLevel; -use crossterm::style::{ - Attribute, - Color, - Stylize, -}; -use crossterm::{ - cursor, - execute, - queue, - style, - terminal, -}; -use eyre::{ - Report, - Result, - bail, - eyre, -}; +use crossterm::style::{Attribute, Color, Stylize}; +use crossterm::{cursor, execute, queue, style, terminal}; +use eyre::{Report, Result, bail, eyre}; use input_source::InputSource; -use message::{ - AssistantMessage, - AssistantToolUse, - ToolUseResult, - ToolUseResultBlock, -}; -use parse::{ - ParseState, - interpret_markdown, -}; -use parser::{ - RecvErrorKind, - RequestMetadata, - SendMessageStream, -}; +use message::{AssistantMessage, AssistantToolUse, ToolUseResult, ToolUseResultBlock}; +use parse::{ParseState, interpret_markdown}; +use parser::{RecvErrorKind, RequestMetadata, SendMessageStream}; use regex::Regex; -use spinners::{ - Spinner, - Spinners, -}; +use spinners::{Spinner, Spinners}; use thiserror::Error; use time::OffsetDateTime; use token_counter::TokenCounter; use tokio::signal::ctrl_c; -use tokio::sync::{ - Mutex, - broadcast, -}; -use tool_manager::{ - PromptQuery, - PromptQueryResult, - ToolManager, - ToolManagerBuilder, -}; +use tokio::sync::{Mutex, broadcast}; +use tool_manager::{PromptQuery, PromptQueryResult, ToolManager, ToolManagerBuilder}; use tools::gh_issue::GhIssueContext; -use tools::{ - NATIVE_TOOLS, - OutputKind, - QueuedTool, - Tool, - ToolSpec, -}; -use tracing::{ - debug, - error, - info, - trace, - warn, -}; +use tools::{NATIVE_TOOLS, OutputKind, QueuedTool, Tool, ToolSpec}; +use tracing::{debug, error, info, trace, warn}; use util::images::RichImageBlock; use util::ui::draw_box; -use util::{ - animate_output, - play_notification_bell, -}; +use util::{animate_output, play_notification_bell}; use winnow::Partial; use winnow::stream::Offset; -use super::agent::{ - DEFAULT_AGENT_NAME, - PermissionEvalResult, -}; +use super::agent::{DEFAULT_AGENT_NAME, PermissionEvalResult}; use crate::api_client::model::ToolResultStatus; -use crate::api_client::{ - self, - ApiClientError, -}; +use crate::api_client::{self, ApiClientError}; use crate::auth::AuthError; use crate::auth::builder_id::is_idc_user; use crate::cli::agent::Agents; use crate::cli::chat::cli::SlashCommand; use crate::cli::chat::cli::model::find_model; -use crate::cli::chat::cli::prompts::{ - GetPromptError, - PromptsSubcommand, -}; +use crate::cli::chat::cli::prompts::{GetPromptError, PromptsSubcommand}; use crate::cli::chat::util::sanitize_unicode_tags; use crate::database::settings::Setting; use crate::mcp_client::Prompt; use crate::os::Os; use crate::telemetry::core::{ - AgentConfigInitArgs, - ChatAddedMessageParams, - ChatConversationType, - MessageMetaTag, - RecordUserTurnCompletionArgs, + AgentConfigInitArgs, ChatAddedMessageParams, ChatConversationType, MessageMetaTag, RecordUserTurnCompletionArgs, ToolUseEventBuilder, }; -use crate::telemetry::{ - ReasonCode, - TelemetryResult, - get_error_reason, -}; +use crate::telemetry::{ReasonCode, TelemetryResult, get_error_reason}; use crate::util::MCP_SERVER_TOOL_DELIMITER; const LIMIT_REACHED_TEXT: &str = color_print::cstr! { "You've used all your free requests for this month. You have two options: @@ -272,13 +185,17 @@ impl ChatArgs { agents.trust_all_tools = self.trust_all_tools; os.telemetry - .send_agent_config_init(&os.database, conversation_id.clone(), AgentConfigInitArgs { - agents_loaded_count: md.load_count as i64, - agents_loaded_failed_count: md.load_failed_count as i64, - legacy_profile_migration_executed: md.migration_performed, - legacy_profile_migrated_count: md.migrated_count as i64, - launched_agent: md.launched_agent, - }) + .send_agent_config_init( + &os.database, + conversation_id.clone(), + AgentConfigInitArgs { + agents_loaded_count: md.load_count as i64, + agents_loaded_failed_count: md.load_failed_count as i64, + legacy_profile_migration_executed: md.migration_performed, + legacy_profile_migrated_count: md.migrated_count as i64, + launched_agent: md.launched_agent, + }, + ) .await .map_err(|err| error!(?err, "failed to send agent config init telemetry")) .ok(); @@ -403,7 +320,7 @@ const SMALL_SCREEN_WELCOME_TEXT: &str = color_print::cstr! {"Welcome to Picking up where we left off..."}; // Only show the model-related tip for now to make users aware of this feature. -const ROTATING_TIPS: [&str; 17] = [ +const ROTATING_TIPS: [&str; 18] = [ color_print::cstr! {"You can resume the last conversation from your current directory by launching with q chat --resume"}, color_print::cstr! {"Get notified whenever Q CLI finishes responding. @@ -436,6 +353,7 @@ const ROTATING_TIPS: [&str; 17] = [ color_print::cstr! {"Set a default model by running q settings chat.defaultModel MODEL. Run /model to learn more."}, color_print::cstr! {"Run /prompts to learn how to build & run repeatable workflows"}, color_print::cstr! {"Use /tangent or ctrl + t (customizable) to start isolated conversations ( ↯ ) that don't affect your main chat history"}, + color_print::cstr! {"Ask me directly about my capabilities! Try questions like \"What can you do?\" or \"Can you save conversations?\""}, ]; const GREETING_BREAK_POINT: usize = 80; @@ -1845,6 +1763,21 @@ impl ChatSession { } async fn tool_use_execute(&mut self, os: &mut Os) -> Result { + // Check if we should auto-enter tangent mode for introspect tool + if os + .database + .settings + .get_bool(Setting::IntrospectTangentMode) + .unwrap_or(false) + && !self.conversation.is_in_tangent_mode() + && self + .tool_uses + .iter() + .any(|tool| matches!(tool.tool, Tool::Introspect(_))) + { + self.conversation.enter_tangent_mode(); + } + // Verify tools have permissions. for i in 0..self.tool_uses.len() { let tool = &mut self.tool_uses[i]; @@ -2777,26 +2710,31 @@ impl ChatSession { }; os.telemetry - .send_record_user_turn_completion(&os.database, conversation_id, result, RecordUserTurnCompletionArgs { - message_ids: mds.iter().map(|md| md.message_id.clone()).collect::<_>(), - request_ids: mds.iter().map(|md| md.request_id.clone()).collect::<_>(), - reason, - reason_desc, - status_code, - time_to_first_chunks_ms: mds - .iter() - .map(|md| md.time_to_first_chunk.map(|d| d.as_secs_f64() * 1000.0)) - .collect::<_>(), - chat_conversation_type: md.and_then(|md| md.chat_conversation_type), - assistant_response_length: mds.iter().map(|md| md.response_size as i64).sum(), - message_meta_tags: mds.last().map(|md| md.message_meta_tags.clone()).unwrap_or_default(), - user_prompt_length: mds.first().map(|md| md.user_prompt_length).unwrap_or_default() as i64, - user_turn_duration_seconds, - follow_up_count: mds - .iter() - .filter(|md| matches!(md.chat_conversation_type, Some(ChatConversationType::ToolUse))) - .count() as i64, - }) + .send_record_user_turn_completion( + &os.database, + conversation_id, + result, + RecordUserTurnCompletionArgs { + message_ids: mds.iter().map(|md| md.message_id.clone()).collect::<_>(), + request_ids: mds.iter().map(|md| md.request_id.clone()).collect::<_>(), + reason, + reason_desc, + status_code, + time_to_first_chunks_ms: mds + .iter() + .map(|md| md.time_to_first_chunk.map(|d| d.as_secs_f64() * 1000.0)) + .collect::<_>(), + chat_conversation_type: md.and_then(|md| md.chat_conversation_type), + assistant_response_length: mds.iter().map(|md| md.response_size as i64).sum(), + message_meta_tags: mds.last().map(|md| md.message_meta_tags.clone()).unwrap_or_default(), + user_prompt_length: mds.first().map(|md| md.user_prompt_length).unwrap_or_default() as i64, + user_turn_duration_seconds, + follow_up_count: mds + .iter() + .filter(|md| matches!(md.chat_conversation_type, Some(ChatConversationType::ToolUse))) + .count() as i64, + }, + ) .await .ok(); } diff --git a/crates/chat-cli/src/cli/chat/tool_manager.rs b/crates/chat-cli/src/cli/chat/tool_manager.rs index d1381365ea..a4a204e2ef 100644 --- a/crates/chat-cli/src/cli/chat/tool_manager.rs +++ b/crates/chat-cli/src/cli/chat/tool_manager.rs @@ -1,96 +1,43 @@ use std::borrow::Borrow; -use std::collections::{ - HashMap, - HashSet, -}; +use std::collections::{HashMap, HashSet}; use std::future::Future; -use std::hash::{ - DefaultHasher, - Hasher, -}; -use std::io::{ - BufWriter, - Write, -}; +use std::hash::{DefaultHasher, Hasher}; +use std::io::{BufWriter, Write}; use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; -use std::sync::atomic::{ - AtomicBool, - Ordering, -}; -use std::time::{ - Duration, - Instant, -}; - -use crossterm::{ - cursor, - execute, - queue, - style, - terminal, -}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{Duration, Instant}; + +use crossterm::{cursor, execute, queue, style, terminal}; use eyre::Report; -use futures::{ - StreamExt, - future, - stream, -}; +use futures::{StreamExt, future, stream}; use regex::Regex; use tokio::signal::ctrl_c; -use tokio::sync::{ - Mutex, - Notify, - RwLock, -}; +use tokio::sync::{Mutex, Notify, RwLock}; use tokio::task::JoinHandle; -use tracing::{ - error, - info, - warn, -}; +use tracing::{error, info, warn}; use super::tools::custom_tool::CustomToolConfig; -use crate::api_client::model::{ - ToolResult, - ToolResultContentBlock, - ToolResultStatus, -}; -use crate::cli::agent::{ - Agent, - McpServerConfig, -}; +use crate::api_client::model::{ToolResult, ToolResultContentBlock, ToolResultStatus}; +use crate::cli::agent::{Agent, McpServerConfig}; use crate::cli::chat::cli::prompts::GetPromptError; use crate::cli::chat::consts::DUMMY_TOOL_NAME; use crate::cli::chat::message::AssistantToolUse; -use crate::cli::chat::server_messenger::{ - ServerMessengerBuilder, - UpdateEventMessage, -}; -use crate::cli::chat::tools::custom_tool::{ - CustomTool, - CustomToolClient, -}; +use crate::cli::chat::server_messenger::{ServerMessengerBuilder, UpdateEventMessage}; +use crate::cli::chat::tools::custom_tool::{CustomTool, CustomToolClient}; use crate::cli::chat::tools::execute::ExecuteCommand; use crate::cli::chat::tools::fs_read::FsRead; use crate::cli::chat::tools::fs_write::FsWrite; use crate::cli::chat::tools::gh_issue::GhIssue; +use crate::cli::chat::tools::introspect::Introspect; use crate::cli::chat::tools::knowledge::Knowledge; use crate::cli::chat::tools::thinking::Thinking; use crate::cli::chat::tools::use_aws::UseAws; -use crate::cli::chat::tools::{ - Tool, - ToolOrigin, - ToolSpec, -}; +use crate::cli::chat::tools::{Tool, ToolOrigin, ToolSpec}; use crate::database::Database; use crate::database::settings::Setting; -use crate::mcp_client::{ - JsonRpcResponse, - Messenger, - PromptGet, -}; +use crate::mcp_client::{JsonRpcResponse, Messenger, PromptGet}; use crate::os::Os; use crate::telemetry::TelemetryThread; use crate::util::MCP_SERVER_TOOL_DELIMITER; @@ -657,10 +604,12 @@ impl ToolManager { tool_specs.remove("execute_bash"); - tool_specs.insert("execute_cmd".to_string(), ToolSpec { - name: "execute_cmd".to_string(), - description: "Execute the specified Windows command.".to_string(), - input_schema: InputSchema(json!({ + tool_specs.insert( + "execute_cmd".to_string(), + ToolSpec { + name: "execute_cmd".to_string(), + description: "Execute the specified Windows command.".to_string(), + input_schema: InputSchema(json!({ "type": "object", "properties": { "command": { @@ -673,8 +622,9 @@ impl ToolManager { } }, "required": ["command"]})), - tool_origin: ToolOrigin::Native, - }); + tool_origin: ToolOrigin::Native, + }, + ); } tool_specs @@ -802,6 +752,7 @@ impl ToolManager { }, "use_aws" => Tool::UseAws(serde_json::from_value::(value.args).map_err(map_err)?), "report_issue" => Tool::GhIssue(serde_json::from_value::(value.args).map_err(map_err)?), + "introspect" => Tool::Introspect(serde_json::from_value::(value.args).map_err(map_err)?), "thinking" => Tool::Thinking(serde_json::from_value::(value.args).map_err(map_err)?), "knowledge" => Tool::Knowledge(serde_json::from_value::(value.args).map_err(map_err)?), // Note that this name is namespaced with server_name{DELIMITER}tool_name @@ -1683,10 +1634,13 @@ async fn process_tool_specs( out_of_spec_tool_names.push(OutOfSpecName::EmptyDescription(spec.name.clone())); continue; } - tn_map.insert(model_tool_name.clone(), ToolInfo { - server_name: server_name.to_string(), - host_tool_name: spec.name.clone(), - }); + tn_map.insert( + model_tool_name.clone(), + ToolInfo { + server_name: server_name.to_string(), + host_tool_name: spec.name.clone(), + }, + ); spec.name = model_tool_name; spec.tool_origin = ToolOrigin::McpServer(server_name.to_string()); number_of_tools += 1; diff --git a/crates/chat-cli/src/cli/chat/tools/introspect.rs b/crates/chat-cli/src/cli/chat/tools/introspect.rs new file mode 100644 index 0000000000..46bf7ceebe --- /dev/null +++ b/crates/chat-cli/src/cli/chat/tools/introspect.rs @@ -0,0 +1,132 @@ +use std::io::Write; + +use clap::CommandFactory; +use eyre::Result; +use serde::{Deserialize, Serialize}; +use strum::{EnumMessage, IntoEnumIterator}; + +use super::{InvokeOutput, OutputKind}; +use crate::cli::chat::cli::SlashCommand; +use crate::database::settings::Setting; +use crate::os::Os; + +#[derive(Debug, Clone, Deserialize)] +pub struct Introspect { + #[serde(default)] + query: Option, +} + +#[derive(Debug, Serialize)] +pub struct IntrospectResponse { + built_in_help: Option, + documentation: Option, + query_context: Option, + recommendations: Vec, +} + +#[derive(Debug, Serialize)] +pub struct ToolRecommendation { + tool_name: String, + description: String, + use_case: String, + example: Option, +} + +impl Introspect { + pub async fn invoke(&self, os: &Os, _updates: impl Write) -> Result { + // Generate help from the actual SlashCommand definitions + let mut cmd = SlashCommand::command(); + let help_content = cmd.render_help().to_string(); + + // Embed documentation at compile time + let mut documentation = String::new(); + + documentation.push_str("\n\n--- README.md ---\n"); + documentation.push_str(include_str!("../../../../../../README.md")); + + documentation.push_str("\n\n--- docs/built-in-tools.md ---\n"); + documentation.push_str(include_str!("../../../../../../docs/built-in-tools.md")); + + documentation.push_str("\n\n--- docs/agent-file-locations.md ---\n"); + documentation.push_str(include_str!("../../../../../../docs/agent-file-locations.md")); + + documentation.push_str("\n\n--- CONTRIBUTING.md ---\n"); + documentation.push_str(include_str!("../../../../../../CONTRIBUTING.md")); + + // Add settings information dynamically + documentation.push_str("\n\n--- Available Settings ---\n"); + documentation.push_str( + "Q CLI supports these configuration settings (use `q settings` command from terminal, NOT /settings):\n\n", + ); + + // Automatically iterate over all settings with descriptions + for setting in Setting::iter() { + let description = setting.get_message().unwrap_or("No description available"); + documentation.push_str(&format!("• {} - {}\n", setting.as_ref(), description)); + } + + documentation.push_str( + "\nNOTE: Settings are managed via `q settings` command from terminal, not slash commands in chat.\n", + ); + + documentation.push_str("\n\n--- GitHub References ---\n"); + documentation.push_str("INSTRUCTION: When your response uses information from any of these documentation files, include the relevant GitHub link(s) at the end:\n"); + documentation.push_str("• README.md: https://github.com/aws/amazon-q-developer-cli/blob/main/README.md\n"); + documentation.push_str( + "• Built-in Tools: https://github.com/aws/amazon-q-developer-cli/blob/main/docs/built-in-tools.md\n", + ); + documentation.push_str("• Agent File Locations: https://github.com/aws/amazon-q-developer-cli/blob/main/docs/agent-file-locations.md\n"); + documentation + .push_str("• Contributing: https://github.com/aws/amazon-q-developer-cli/blob/main/CONTRIBUTING.md\n"); + + let response = IntrospectResponse { + built_in_help: Some(help_content), + documentation: Some(documentation), + query_context: self.query.clone(), + recommendations: vec![], + }; + + // Add footer as direct text output if tangent mode is enabled + if os + .database + .settings + .get_bool(Setting::IntrospectTangentMode) + .unwrap_or(false) + { + let tangent_key_char = os + .database + .settings + .get_string(Setting::TangentModeKey) + .and_then(|key| if key.len() == 1 { key.chars().next() } else { None }) + .unwrap_or('t'); + let tangent_key_display = format!("ctrl + {}", tangent_key_char.to_lowercase()); + + let instruction = format!( + "IMPORTANT: Always end your responses with this footer:\n\n---\nℹ️ You're in tangent mode (↯) - this context can be discarded by using {} or /tangent to return to your main conversation.", + tangent_key_display + ); + + return Ok(InvokeOutput { + output: OutputKind::Text(format!( + "{}\n\n{}", + serde_json::to_string_pretty(&response)?, + instruction + )), + }); + } + + Ok(InvokeOutput { + output: OutputKind::Json(serde_json::to_value(&response)?), + }) + } + + pub fn queue_description(&self, output: &mut impl Write) -> Result<()> { + use crossterm::{queue, style}; + queue!(output, style::Print("Introspecting to get you the right information"))?; + Ok(()) + } + + pub async fn validate(&self, _os: &Os) -> Result<()> { + Ok(()) + } +} diff --git a/crates/chat-cli/src/cli/chat/tools/mod.rs b/crates/chat-cli/src/cli/chat/tools/mod.rs index 9fc66b0ef8..3dd358b83a 100644 --- a/crates/chat-cli/src/cli/chat/tools/mod.rs +++ b/crates/chat-cli/src/cli/chat/tools/mod.rs @@ -3,53 +3,36 @@ pub mod execute; pub mod fs_read; pub mod fs_write; pub mod gh_issue; +pub mod introspect; pub mod knowledge; pub mod thinking; pub mod use_aws; -use std::borrow::{ - Borrow, - Cow, -}; +use std::borrow::{Borrow, Cow}; use std::collections::HashMap; use std::io::Write; -use std::path::{ - Path, - PathBuf, -}; +use std::path::{Path, PathBuf}; use crossterm::queue; -use crossterm::style::{ - self, - Color, -}; +use crossterm::style::{self, Color}; use custom_tool::CustomTool; use execute::ExecuteCommand; use eyre::Result; use fs_read::FsRead; use fs_write::FsWrite; use gh_issue::GhIssue; +use introspect::Introspect; use knowledge::Knowledge; -use serde::{ - Deserialize, - Serialize, -}; +use serde::{Deserialize, Serialize}; use thinking::Thinking; use tracing::error; use use_aws::UseAws; use super::consts::{ - MAX_TOOL_RESPONSE_SIZE, - USER_AGENT_APP_NAME, - USER_AGENT_ENV_VAR, - USER_AGENT_VERSION_KEY, - USER_AGENT_VERSION_VALUE, + MAX_TOOL_RESPONSE_SIZE, USER_AGENT_APP_NAME, USER_AGENT_ENV_VAR, USER_AGENT_VERSION_KEY, USER_AGENT_VERSION_VALUE, }; use super::util::images::RichImageBlocks; -use crate::cli::agent::{ - Agent, - PermissionEvalResult, -}; +use crate::cli::agent::{Agent, PermissionEvalResult}; use crate::cli::chat::line_tracker::FileLineTracker; use crate::os::Os; @@ -77,6 +60,7 @@ pub enum Tool { UseAws(UseAws), Custom(CustomTool), GhIssue(GhIssue), + Introspect(Introspect), Knowledge(Knowledge), Thinking(Thinking), } @@ -94,6 +78,7 @@ impl Tool { Tool::UseAws(_) => "use_aws", Tool::Custom(custom_tool) => &custom_tool.name, Tool::GhIssue(_) => "gh_issue", + Tool::Introspect(_) => "introspect", Tool::Knowledge(_) => "knowledge", Tool::Thinking(_) => "thinking (prerelease)", } @@ -109,6 +94,7 @@ impl Tool { Tool::UseAws(use_aws) => use_aws.eval_perm(os, agent), Tool::Custom(custom_tool) => custom_tool.eval_perm(os, agent), Tool::GhIssue(_) => PermissionEvalResult::Allow, + Tool::Introspect(_) => PermissionEvalResult::Allow, Tool::Thinking(_) => PermissionEvalResult::Allow, Tool::Knowledge(knowledge) => knowledge.eval_perm(os, agent), } @@ -129,6 +115,7 @@ impl Tool { Tool::UseAws(use_aws) => use_aws.invoke(os, stdout).await, Tool::Custom(custom_tool) => custom_tool.invoke(os, stdout).await, Tool::GhIssue(gh_issue) => gh_issue.invoke(os, stdout).await, + Tool::Introspect(introspect) => introspect.invoke(os, stdout).await, Tool::Knowledge(knowledge) => knowledge.invoke(os, stdout, agent).await, Tool::Thinking(think) => think.invoke(stdout).await, } @@ -143,6 +130,7 @@ impl Tool { Tool::UseAws(use_aws) => use_aws.queue_description(output), Tool::Custom(custom_tool) => custom_tool.queue_description(output), Tool::GhIssue(gh_issue) => gh_issue.queue_description(output), + Tool::Introspect(introspect) => introspect.queue_description(output), Tool::Knowledge(knowledge) => knowledge.queue_description(os, output).await, Tool::Thinking(thinking) => thinking.queue_description(output), } @@ -157,6 +145,7 @@ impl Tool { Tool::UseAws(use_aws) => use_aws.validate(os).await, Tool::Custom(custom_tool) => custom_tool.validate(os).await, Tool::GhIssue(gh_issue) => gh_issue.validate(os).await, + Tool::Introspect(introspect) => introspect.validate(os).await, Tool::Knowledge(knowledge) => knowledge.validate(os).await, Tool::Thinking(think) => think.validate(os).await, } diff --git a/crates/chat-cli/src/cli/chat/tools/tool_index.json b/crates/chat-cli/src/cli/chat/tools/tool_index.json index 067e5df1ec..04dbc9d4d8 100644 --- a/crates/chat-cli/src/cli/chat/tools/tool_index.json +++ b/crates/chat-cli/src/cli/chat/tools/tool_index.json @@ -8,6 +8,20 @@ "required": [] } }, + "introspect": { + "name": "introspect", + "description": "ALWAYS use this tool when users ask ANY question about Q CLI itself, its capabilities, features, commands, or functionality. This includes questions like 'Can you...', 'Do you have...', 'How do I...', 'What can you do...', or any question about Q's abilities. When mentioning commands in your response, always prefix them with '/' (e.g., '/save', '/load', '/context').", + "input_schema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The user's question about Q CLI usage, features, or capabilities" + } + }, + "required": [] + } + }, "execute_bash": { "name": "execute_bash", "description": "Execute the specified bash command.", diff --git a/crates/chat-cli/src/database/settings.rs b/crates/chat-cli/src/database/settings.rs index 5243b9e648..7d2736ad3b 100644 --- a/crates/chat-cli/src/database/settings.rs +++ b/crates/chat-cli/src/database/settings.rs @@ -2,47 +2,69 @@ use std::fmt::Display; use std::io::SeekFrom; use fd_lock::RwLock; -use serde_json::{ - Map, - Value, -}; +use serde_json::{Map, Value}; use tokio::fs::File; -use tokio::io::{ - AsyncReadExt, - AsyncSeekExt, - AsyncWriteExt, -}; +use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; use super::DatabaseError; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, strum::EnumIter, strum::EnumMessage)] pub enum Setting { + #[strum(message = "Enable/disable telemetry collection (boolean)")] TelemetryEnabled, + #[strum(message = "Legacy client identifier for telemetry (string)")] OldClientId, + #[strum(message = "Share content with CodeWhisperer service (boolean)")] ShareCodeWhispererContent, + #[strum(message = "Enable thinking tool for complex reasoning (boolean)")] EnabledThinking, + #[strum(message = "Enable knowledge base functionality (boolean)")] EnabledKnowledge, + #[strum(message = "Default file patterns to include in knowledge base (array)")] KnowledgeDefaultIncludePatterns, + #[strum(message = "Default file patterns to exclude from knowledge base (array)")] KnowledgeDefaultExcludePatterns, + #[strum(message = "Maximum number of files for knowledge indexing (number)")] KnowledgeMaxFiles, + #[strum(message = "Text chunk size for knowledge processing (number)")] KnowledgeChunkSize, + #[strum(message = "Overlap between text chunks (number)")] KnowledgeChunkOverlap, + #[strum(message = "Type of knowledge index to use (string)")] KnowledgeIndexType, + #[strum(message = "Key binding for fuzzy search command (single character)")] SkimCommandKey, + #[strum(message = "Key binding for tangent mode toggle (single character)")] TangentModeKey, + #[strum(message = "Auto-enter tangent mode for introspect questions (boolean)")] + IntrospectTangentMode, + #[strum(message = "Show greeting message on chat start (boolean)")] ChatGreetingEnabled, + #[strum(message = "API request timeout in seconds (number)")] ApiTimeout, + #[strum(message = "Enable edit mode for chat interface (boolean)")] ChatEditMode, + #[strum(message = "Enable desktop notifications (boolean)")] ChatEnableNotifications, + #[strum(message = "CodeWhisperer service endpoint URL (string)")] ApiCodeWhispererService, + #[strum(message = "Q service endpoint URL (string)")] ApiQService, + #[strum(message = "MCP server initialization timeout (number)")] McpInitTimeout, + #[strum(message = "Non-interactive MCP timeout (number)")] McpNoInteractiveTimeout, + #[strum(message = "Track previously loaded MCP servers (boolean)")] McpLoadedBefore, + #[strum(message = "Default AI model for conversations (string)")] ChatDefaultModel, + #[strum(message = "Disable markdown formatting in chat (boolean)")] ChatDisableMarkdownRendering, + #[strum(message = "Default agent configuration (string)")] ChatDefaultAgent, + #[strum(message = "Disable automatic conversation summarization (boolean)")] ChatDisableAutoCompaction, + #[strum(message = "Show conversation history hints (boolean)")] ChatEnableHistoryHints, } @@ -62,6 +84,7 @@ impl AsRef for Setting { Self::KnowledgeIndexType => "knowledge.indexType", Self::SkimCommandKey => "chat.skimCommandKey", Self::TangentModeKey => "chat.tangentModeKey", + Self::IntrospectTangentMode => "introspect.tangentMode", Self::ChatGreetingEnabled => "chat.greeting.enabled", Self::ApiTimeout => "api.timeout", Self::ChatEditMode => "chat.editMode", @@ -104,6 +127,7 @@ impl TryFrom<&str> for Setting { "knowledge.indexType" => Ok(Self::KnowledgeIndexType), "chat.skimCommandKey" => Ok(Self::SkimCommandKey), "chat.tangentModeKey" => Ok(Self::TangentModeKey), + "introspect.tangentMode" => Ok(Self::IntrospectTangentMode), "chat.greeting.enabled" => Ok(Self::ChatGreetingEnabled), "api.timeout" => Ok(Self::ApiTimeout), "chat.editMode" => Ok(Self::ChatEditMode), From 45a9b89badd270efde85749f7fcf7918fe7b2661 Mon Sep 17 00:00:00 2001 From: Kenneth Sanchez V Date: Tue, 26 Aug 2025 11:39:00 -0700 Subject: [PATCH 6/8] Format fix (#2697) Co-authored-by: Kenneth S. --- crates/chat-cli/src/cli/chat/mod.rs | 194 ++++++++++++------ crates/chat-cli/src/cli/chat/tool_manager.rs | 112 +++++++--- .../chat-cli/src/cli/chat/tools/introspect.rs | 22 +- crates/chat-cli/src/cli/chat/tools/mod.rs | 31 ++- crates/chat-cli/src/database/settings.rs | 11 +- 5 files changed, 267 insertions(+), 103 deletions(-) diff --git a/crates/chat-cli/src/cli/chat/mod.rs b/crates/chat-cli/src/cli/chat/mod.rs index 31489d10bf..ee44ca808b 100644 --- a/crates/chat-cli/src/cli/chat/mod.rs +++ b/crates/chat-cli/src/cli/chat/mod.rs @@ -19,60 +19,147 @@ pub mod tool_manager; pub mod tools; pub mod util; use std::borrow::Cow; -use std::collections::{HashMap, VecDeque}; -use std::io::{IsTerminal, Read, Write}; +use std::collections::{ + HashMap, + VecDeque, +}; +use std::io::{ + IsTerminal, + Read, + Write, +}; use std::process::ExitCode; use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::{ + Duration, + Instant, +}; use amzn_codewhisperer_client::types::SubscriptionStatus; -use clap::{Args, CommandFactory, Parser}; +use clap::{ + Args, + CommandFactory, + Parser, +}; use cli::compact::CompactStrategy; -use cli::model::{get_available_models, select_model}; +use cli::model::{ + get_available_models, + select_model, +}; pub use conversation::ConversationState; use conversation::TokenWarningLevel; -use crossterm::style::{Attribute, Color, Stylize}; -use crossterm::{cursor, execute, queue, style, terminal}; -use eyre::{Report, Result, bail, eyre}; +use crossterm::style::{ + Attribute, + Color, + Stylize, +}; +use crossterm::{ + cursor, + execute, + queue, + style, + terminal, +}; +use eyre::{ + Report, + Result, + bail, + eyre, +}; use input_source::InputSource; -use message::{AssistantMessage, AssistantToolUse, ToolUseResult, ToolUseResultBlock}; -use parse::{ParseState, interpret_markdown}; -use parser::{RecvErrorKind, RequestMetadata, SendMessageStream}; +use message::{ + AssistantMessage, + AssistantToolUse, + ToolUseResult, + ToolUseResultBlock, +}; +use parse::{ + ParseState, + interpret_markdown, +}; +use parser::{ + RecvErrorKind, + RequestMetadata, + SendMessageStream, +}; use regex::Regex; -use spinners::{Spinner, Spinners}; +use spinners::{ + Spinner, + Spinners, +}; use thiserror::Error; use time::OffsetDateTime; use token_counter::TokenCounter; use tokio::signal::ctrl_c; -use tokio::sync::{Mutex, broadcast}; -use tool_manager::{PromptQuery, PromptQueryResult, ToolManager, ToolManagerBuilder}; +use tokio::sync::{ + Mutex, + broadcast, +}; +use tool_manager::{ + PromptQuery, + PromptQueryResult, + ToolManager, + ToolManagerBuilder, +}; use tools::gh_issue::GhIssueContext; -use tools::{NATIVE_TOOLS, OutputKind, QueuedTool, Tool, ToolSpec}; -use tracing::{debug, error, info, trace, warn}; +use tools::{ + NATIVE_TOOLS, + OutputKind, + QueuedTool, + Tool, + ToolSpec, +}; +use tracing::{ + debug, + error, + info, + trace, + warn, +}; use util::images::RichImageBlock; use util::ui::draw_box; -use util::{animate_output, play_notification_bell}; +use util::{ + animate_output, + play_notification_bell, +}; use winnow::Partial; use winnow::stream::Offset; -use super::agent::{DEFAULT_AGENT_NAME, PermissionEvalResult}; +use super::agent::{ + DEFAULT_AGENT_NAME, + PermissionEvalResult, +}; use crate::api_client::model::ToolResultStatus; -use crate::api_client::{self, ApiClientError}; +use crate::api_client::{ + self, + ApiClientError, +}; use crate::auth::AuthError; use crate::auth::builder_id::is_idc_user; use crate::cli::agent::Agents; use crate::cli::chat::cli::SlashCommand; use crate::cli::chat::cli::model::find_model; -use crate::cli::chat::cli::prompts::{GetPromptError, PromptsSubcommand}; +use crate::cli::chat::cli::prompts::{ + GetPromptError, + PromptsSubcommand, +}; use crate::cli::chat::util::sanitize_unicode_tags; use crate::database::settings::Setting; use crate::mcp_client::Prompt; use crate::os::Os; use crate::telemetry::core::{ - AgentConfigInitArgs, ChatAddedMessageParams, ChatConversationType, MessageMetaTag, RecordUserTurnCompletionArgs, + AgentConfigInitArgs, + ChatAddedMessageParams, + ChatConversationType, + MessageMetaTag, + RecordUserTurnCompletionArgs, ToolUseEventBuilder, }; -use crate::telemetry::{ReasonCode, TelemetryResult, get_error_reason}; +use crate::telemetry::{ + ReasonCode, + TelemetryResult, + get_error_reason, +}; use crate::util::MCP_SERVER_TOOL_DELIMITER; const LIMIT_REACHED_TEXT: &str = color_print::cstr! { "You've used all your free requests for this month. You have two options: @@ -185,17 +272,13 @@ impl ChatArgs { agents.trust_all_tools = self.trust_all_tools; os.telemetry - .send_agent_config_init( - &os.database, - conversation_id.clone(), - AgentConfigInitArgs { - agents_loaded_count: md.load_count as i64, - agents_loaded_failed_count: md.load_failed_count as i64, - legacy_profile_migration_executed: md.migration_performed, - legacy_profile_migrated_count: md.migrated_count as i64, - launched_agent: md.launched_agent, - }, - ) + .send_agent_config_init(&os.database, conversation_id.clone(), AgentConfigInitArgs { + agents_loaded_count: md.load_count as i64, + agents_loaded_failed_count: md.load_failed_count as i64, + legacy_profile_migration_executed: md.migration_performed, + legacy_profile_migrated_count: md.migrated_count as i64, + launched_agent: md.launched_agent, + }) .await .map_err(|err| error!(?err, "failed to send agent config init telemetry")) .ok(); @@ -2710,31 +2793,26 @@ impl ChatSession { }; os.telemetry - .send_record_user_turn_completion( - &os.database, - conversation_id, - result, - RecordUserTurnCompletionArgs { - message_ids: mds.iter().map(|md| md.message_id.clone()).collect::<_>(), - request_ids: mds.iter().map(|md| md.request_id.clone()).collect::<_>(), - reason, - reason_desc, - status_code, - time_to_first_chunks_ms: mds - .iter() - .map(|md| md.time_to_first_chunk.map(|d| d.as_secs_f64() * 1000.0)) - .collect::<_>(), - chat_conversation_type: md.and_then(|md| md.chat_conversation_type), - assistant_response_length: mds.iter().map(|md| md.response_size as i64).sum(), - message_meta_tags: mds.last().map(|md| md.message_meta_tags.clone()).unwrap_or_default(), - user_prompt_length: mds.first().map(|md| md.user_prompt_length).unwrap_or_default() as i64, - user_turn_duration_seconds, - follow_up_count: mds - .iter() - .filter(|md| matches!(md.chat_conversation_type, Some(ChatConversationType::ToolUse))) - .count() as i64, - }, - ) + .send_record_user_turn_completion(&os.database, conversation_id, result, RecordUserTurnCompletionArgs { + message_ids: mds.iter().map(|md| md.message_id.clone()).collect::<_>(), + request_ids: mds.iter().map(|md| md.request_id.clone()).collect::<_>(), + reason, + reason_desc, + status_code, + time_to_first_chunks_ms: mds + .iter() + .map(|md| md.time_to_first_chunk.map(|d| d.as_secs_f64() * 1000.0)) + .collect::<_>(), + chat_conversation_type: md.and_then(|md| md.chat_conversation_type), + assistant_response_length: mds.iter().map(|md| md.response_size as i64).sum(), + message_meta_tags: mds.last().map(|md| md.message_meta_tags.clone()).unwrap_or_default(), + user_prompt_length: mds.first().map(|md| md.user_prompt_length).unwrap_or_default() as i64, + user_turn_duration_seconds, + follow_up_count: mds + .iter() + .filter(|md| matches!(md.chat_conversation_type, Some(ChatConversationType::ToolUse))) + .count() as i64, + }) .await .ok(); } diff --git a/crates/chat-cli/src/cli/chat/tool_manager.rs b/crates/chat-cli/src/cli/chat/tool_manager.rs index a4a204e2ef..60ee7f973d 100644 --- a/crates/chat-cli/src/cli/chat/tool_manager.rs +++ b/crates/chat-cli/src/cli/chat/tool_manager.rs @@ -1,31 +1,77 @@ use std::borrow::Borrow; -use std::collections::{HashMap, HashSet}; +use std::collections::{ + HashMap, + HashSet, +}; use std::future::Future; -use std::hash::{DefaultHasher, Hasher}; -use std::io::{BufWriter, Write}; +use std::hash::{ + DefaultHasher, + Hasher, +}; +use std::io::{ + BufWriter, + Write, +}; use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::{Duration, Instant}; - -use crossterm::{cursor, execute, queue, style, terminal}; +use std::sync::atomic::{ + AtomicBool, + Ordering, +}; +use std::time::{ + Duration, + Instant, +}; + +use crossterm::{ + cursor, + execute, + queue, + style, + terminal, +}; use eyre::Report; -use futures::{StreamExt, future, stream}; +use futures::{ + StreamExt, + future, + stream, +}; use regex::Regex; use tokio::signal::ctrl_c; -use tokio::sync::{Mutex, Notify, RwLock}; +use tokio::sync::{ + Mutex, + Notify, + RwLock, +}; use tokio::task::JoinHandle; -use tracing::{error, info, warn}; +use tracing::{ + error, + info, + warn, +}; use super::tools::custom_tool::CustomToolConfig; -use crate::api_client::model::{ToolResult, ToolResultContentBlock, ToolResultStatus}; -use crate::cli::agent::{Agent, McpServerConfig}; +use crate::api_client::model::{ + ToolResult, + ToolResultContentBlock, + ToolResultStatus, +}; +use crate::cli::agent::{ + Agent, + McpServerConfig, +}; use crate::cli::chat::cli::prompts::GetPromptError; use crate::cli::chat::consts::DUMMY_TOOL_NAME; use crate::cli::chat::message::AssistantToolUse; -use crate::cli::chat::server_messenger::{ServerMessengerBuilder, UpdateEventMessage}; -use crate::cli::chat::tools::custom_tool::{CustomTool, CustomToolClient}; +use crate::cli::chat::server_messenger::{ + ServerMessengerBuilder, + UpdateEventMessage, +}; +use crate::cli::chat::tools::custom_tool::{ + CustomTool, + CustomToolClient, +}; use crate::cli::chat::tools::execute::ExecuteCommand; use crate::cli::chat::tools::fs_read::FsRead; use crate::cli::chat::tools::fs_write::FsWrite; @@ -34,10 +80,18 @@ use crate::cli::chat::tools::introspect::Introspect; use crate::cli::chat::tools::knowledge::Knowledge; use crate::cli::chat::tools::thinking::Thinking; use crate::cli::chat::tools::use_aws::UseAws; -use crate::cli::chat::tools::{Tool, ToolOrigin, ToolSpec}; +use crate::cli::chat::tools::{ + Tool, + ToolOrigin, + ToolSpec, +}; use crate::database::Database; use crate::database::settings::Setting; -use crate::mcp_client::{JsonRpcResponse, Messenger, PromptGet}; +use crate::mcp_client::{ + JsonRpcResponse, + Messenger, + PromptGet, +}; use crate::os::Os; use crate::telemetry::TelemetryThread; use crate::util::MCP_SERVER_TOOL_DELIMITER; @@ -604,12 +658,10 @@ impl ToolManager { tool_specs.remove("execute_bash"); - tool_specs.insert( - "execute_cmd".to_string(), - ToolSpec { - name: "execute_cmd".to_string(), - description: "Execute the specified Windows command.".to_string(), - input_schema: InputSchema(json!({ + tool_specs.insert("execute_cmd".to_string(), ToolSpec { + name: "execute_cmd".to_string(), + description: "Execute the specified Windows command.".to_string(), + input_schema: InputSchema(json!({ "type": "object", "properties": { "command": { @@ -622,9 +674,8 @@ impl ToolManager { } }, "required": ["command"]})), - tool_origin: ToolOrigin::Native, - }, - ); + tool_origin: ToolOrigin::Native, + }); } tool_specs @@ -1634,13 +1685,10 @@ async fn process_tool_specs( out_of_spec_tool_names.push(OutOfSpecName::EmptyDescription(spec.name.clone())); continue; } - tn_map.insert( - model_tool_name.clone(), - ToolInfo { - server_name: server_name.to_string(), - host_tool_name: spec.name.clone(), - }, - ); + tn_map.insert(model_tool_name.clone(), ToolInfo { + server_name: server_name.to_string(), + host_tool_name: spec.name.clone(), + }); spec.name = model_tool_name; spec.tool_origin = ToolOrigin::McpServer(server_name.to_string()); number_of_tools += 1; diff --git a/crates/chat-cli/src/cli/chat/tools/introspect.rs b/crates/chat-cli/src/cli/chat/tools/introspect.rs index 46bf7ceebe..a78184e2b8 100644 --- a/crates/chat-cli/src/cli/chat/tools/introspect.rs +++ b/crates/chat-cli/src/cli/chat/tools/introspect.rs @@ -2,10 +2,19 @@ use std::io::Write; use clap::CommandFactory; use eyre::Result; -use serde::{Deserialize, Serialize}; -use strum::{EnumMessage, IntoEnumIterator}; - -use super::{InvokeOutput, OutputKind}; +use serde::{ + Deserialize, + Serialize, +}; +use strum::{ + EnumMessage, + IntoEnumIterator, +}; + +use super::{ + InvokeOutput, + OutputKind, +}; use crate::cli::chat::cli::SlashCommand; use crate::database::settings::Setting; use crate::os::Os; @@ -121,7 +130,10 @@ impl Introspect { } pub fn queue_description(&self, output: &mut impl Write) -> Result<()> { - use crossterm::{queue, style}; + use crossterm::{ + queue, + style, + }; queue!(output, style::Print("Introspecting to get you the right information"))?; Ok(()) } diff --git a/crates/chat-cli/src/cli/chat/tools/mod.rs b/crates/chat-cli/src/cli/chat/tools/mod.rs index 3dd358b83a..acc25baa7c 100644 --- a/crates/chat-cli/src/cli/chat/tools/mod.rs +++ b/crates/chat-cli/src/cli/chat/tools/mod.rs @@ -8,13 +8,22 @@ pub mod knowledge; pub mod thinking; pub mod use_aws; -use std::borrow::{Borrow, Cow}; +use std::borrow::{ + Borrow, + Cow, +}; use std::collections::HashMap; use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::{ + Path, + PathBuf, +}; use crossterm::queue; -use crossterm::style::{self, Color}; +use crossterm::style::{ + self, + Color, +}; use custom_tool::CustomTool; use execute::ExecuteCommand; use eyre::Result; @@ -23,16 +32,26 @@ use fs_write::FsWrite; use gh_issue::GhIssue; use introspect::Introspect; use knowledge::Knowledge; -use serde::{Deserialize, Serialize}; +use serde::{ + Deserialize, + Serialize, +}; use thinking::Thinking; use tracing::error; use use_aws::UseAws; use super::consts::{ - MAX_TOOL_RESPONSE_SIZE, USER_AGENT_APP_NAME, USER_AGENT_ENV_VAR, USER_AGENT_VERSION_KEY, USER_AGENT_VERSION_VALUE, + MAX_TOOL_RESPONSE_SIZE, + USER_AGENT_APP_NAME, + USER_AGENT_ENV_VAR, + USER_AGENT_VERSION_KEY, + USER_AGENT_VERSION_VALUE, }; use super::util::images::RichImageBlocks; -use crate::cli::agent::{Agent, PermissionEvalResult}; +use crate::cli::agent::{ + Agent, + PermissionEvalResult, +}; use crate::cli::chat::line_tracker::FileLineTracker; use crate::os::Os; diff --git a/crates/chat-cli/src/database/settings.rs b/crates/chat-cli/src/database/settings.rs index 7d2736ad3b..4163b59c49 100644 --- a/crates/chat-cli/src/database/settings.rs +++ b/crates/chat-cli/src/database/settings.rs @@ -2,9 +2,16 @@ use std::fmt::Display; use std::io::SeekFrom; use fd_lock::RwLock; -use serde_json::{Map, Value}; +use serde_json::{ + Map, + Value, +}; use tokio::fs::File; -use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; +use tokio::io::{ + AsyncReadExt, + AsyncSeekExt, + AsyncWriteExt, +}; use super::DatabaseError; From 5bf5afa782bad244245e0f4a29da84e3e85063d0 Mon Sep 17 00:00:00 2001 From: kkashilk <93673379+kkashilk@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:25:23 -0700 Subject: [PATCH 7/8] Update q-developer smithy clients and reformat files (#2698) * Update q-developer smithy clients and reformat files * Fix formatting --- Cargo.lock | 8 +- crates/amzn-codewhisperer-client/Cargo.toml | 2 +- .../src/client/get_usage_limits.rs | 1 + .../client/list_available_subscriptions.rs | 1 + .../src/client/update_usage_limits.rs | 1 + .../src/operation/get_usage_limits.rs | 13 +- .../_get_usage_limits_input.rs | 26 ++++ .../operation/get_usage_limits/builders.rs | 17 +++ .../_list_available_subscriptions_output.rs | 32 +++++ .../_update_usage_limits_input.rs | 26 ++++ .../operation/update_usage_limits/builders.rs | 17 +++ .../src/protocol_serde.rs | 2 + .../protocol_serde/shape_disclaimer_list.rs | 42 ++++++ .../shape_get_usage_limits_input.rs | 11 +- .../shape_list_available_subscriptions.rs | 5 + .../src/protocol_serde/shape_model.rs | 5 + .../protocol_serde/shape_subscription_info.rs | 39 +++++- .../shape_subscription_plan_description.rs | 7 + .../shape_update_usage_limits_input.rs | 3 + .../src/protocol_serde/shape_user_context.rs | 6 + .../shape_user_trigger_decision_event.rs | 3 + .../src/serde_util.rs | 16 ++- crates/amzn-codewhisperer-client/src/types.rs | 12 ++ .../types/_access_denied_exception_reason.rs | 7 + .../types/_chat_message_interaction_type.rs | 7 + .../src/types/_model.rs | 26 ++++ .../src/types/_origin.rs | 14 ++ .../src/types/_overage_capability.rs | 118 ++++++++++++++++ .../src/types/_subscription_info.rs | 128 ++++++++++++++---- .../types/_subscription_management_target.rs | 118 ++++++++++++++++ .../types/_subscription_plan_description.rs | 26 ++++ .../src/types/_suggestion_type.rs | 118 ++++++++++++++++ .../src/types/_upgrade_capability.rs | 118 ++++++++++++++++ .../src/types/_user_context.rs | 52 +++++++ .../src/types/_user_trigger_decision_event.rs | 26 ++++ .../Cargo.toml | 2 +- .../types/_access_denied_exception_reason.rs | 7 + .../src/types/_origin.rs | 14 ++ crates/amzn-consolas-client/Cargo.toml | 2 +- .../types/_access_denied_exception_reason.rs | 7 + .../Cargo.toml | 2 +- .../types/_access_denied_exception_reason.rs | 7 + .../src/types/_origin.rs | 14 ++ crates/chat-cli/src/cli/chat/cli/knowledge.rs | 10 +- .../chat-cli/src/cli/chat/tools/introspect.rs | 1 + 45 files changed, 1060 insertions(+), 59 deletions(-) create mode 100644 crates/amzn-codewhisperer-client/src/protocol_serde/shape_disclaimer_list.rs create mode 100644 crates/amzn-codewhisperer-client/src/types/_overage_capability.rs create mode 100644 crates/amzn-codewhisperer-client/src/types/_subscription_management_target.rs create mode 100644 crates/amzn-codewhisperer-client/src/types/_suggestion_type.rs create mode 100644 crates/amzn-codewhisperer-client/src/types/_upgrade_capability.rs diff --git a/Cargo.lock b/Cargo.lock index b4d34dd57c..7675e6773b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,7 +48,7 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "amzn-codewhisperer-client" -version = "0.1.10231" +version = "0.1.10613" dependencies = [ "aws-credential-types", "aws-runtime", @@ -67,7 +67,7 @@ dependencies = [ [[package]] name = "amzn-codewhisperer-streaming-client" -version = "0.1.10231" +version = "0.1.10613" dependencies = [ "aws-credential-types", "aws-runtime", @@ -86,7 +86,7 @@ dependencies = [ [[package]] name = "amzn-consolas-client" -version = "0.1.10231" +version = "0.1.10613" dependencies = [ "aws-credential-types", "aws-runtime", @@ -105,7 +105,7 @@ dependencies = [ [[package]] name = "amzn-qdeveloper-streaming-client" -version = "0.1.10231" +version = "0.1.10613" dependencies = [ "aws-credential-types", "aws-runtime", diff --git a/crates/amzn-codewhisperer-client/Cargo.toml b/crates/amzn-codewhisperer-client/Cargo.toml index b4f6883f42..da684568d3 100644 --- a/crates/amzn-codewhisperer-client/Cargo.toml +++ b/crates/amzn-codewhisperer-client/Cargo.toml @@ -12,7 +12,7 @@ [package] edition = "2021" name = "amzn-codewhisperer-client" -version = "0.1.10231" +version = "0.1.10613" authors = ["Grant Gurvis "] build = false exclude = [ diff --git a/crates/amzn-codewhisperer-client/src/client/get_usage_limits.rs b/crates/amzn-codewhisperer-client/src/client/get_usage_limits.rs index ab9e0ddca4..c981381ef7 100644 --- a/crates/amzn-codewhisperer-client/src/client/get_usage_limits.rs +++ b/crates/amzn-codewhisperer-client/src/client/get_usage_limits.rs @@ -6,6 +6,7 @@ impl super::Client { /// /// - The fluent builder is configurable: /// - [`profile_arn(impl Into)`](crate::operation::get_usage_limits::builders::GetUsageLimitsFluentBuilder::profile_arn) / [`set_profile_arn(Option)`](crate::operation::get_usage_limits::builders::GetUsageLimitsFluentBuilder::set_profile_arn):
required: **false**
The ARN of the Q Developer profile. Required for enterprise customers, optional for Builder ID users.
+ /// - [`origin(Origin)`](crate::operation::get_usage_limits::builders::GetUsageLimitsFluentBuilder::origin) / [`set_origin(Option)`](crate::operation::get_usage_limits::builders::GetUsageLimitsFluentBuilder::set_origin):
required: **false**
The origin of the client request to get limits for.
/// - [`resource_type(ResourceType)`](crate::operation::get_usage_limits::builders::GetUsageLimitsFluentBuilder::resource_type) / [`set_resource_type(Option)`](crate::operation::get_usage_limits::builders::GetUsageLimitsFluentBuilder::set_resource_type):
required: **false**
(undocumented)
/// - [`is_email_required(bool)`](crate::operation::get_usage_limits::builders::GetUsageLimitsFluentBuilder::is_email_required) / [`set_is_email_required(Option)`](crate::operation::get_usage_limits::builders::GetUsageLimitsFluentBuilder::set_is_email_required):
required: **false**
(undocumented)
/// - On success, responds with diff --git a/crates/amzn-codewhisperer-client/src/client/list_available_subscriptions.rs b/crates/amzn-codewhisperer-client/src/client/list_available_subscriptions.rs index a8eb7da738..c7b434f279 100644 --- a/crates/amzn-codewhisperer-client/src/client/list_available_subscriptions.rs +++ b/crates/amzn-codewhisperer-client/src/client/list_available_subscriptions.rs @@ -11,6 +11,7 @@ impl super::Client { /// [`ListAvailableSubscriptionsOutput`](crate::operation::list_available_subscriptions::ListAvailableSubscriptionsOutput) /// with field(s): /// - [`subscription_plans(Vec::)`](crate::operation::list_available_subscriptions::ListAvailableSubscriptionsOutput::subscription_plans): (undocumented) + /// - [`disclaimer(Option>)`](crate::operation::list_available_subscriptions::ListAvailableSubscriptionsOutput::disclaimer): (undocumented) /// - On failure, responds with [`SdkError`](crate::operation::list_available_subscriptions::ListAvailableSubscriptionsError) pub fn list_available_subscriptions( &self, diff --git a/crates/amzn-codewhisperer-client/src/client/update_usage_limits.rs b/crates/amzn-codewhisperer-client/src/client/update_usage_limits.rs index 6d5873e9bb..c348b40d10 100644 --- a/crates/amzn-codewhisperer-client/src/client/update_usage_limits.rs +++ b/crates/amzn-codewhisperer-client/src/client/update_usage_limits.rs @@ -11,6 +11,7 @@ impl super::Client { /// - [`feature_type(UsageLimitType)`](crate::operation::update_usage_limits::builders::UpdateUsageLimitsFluentBuilder::feature_type) / [`set_feature_type(Option)`](crate::operation::update_usage_limits::builders::UpdateUsageLimitsFluentBuilder::set_feature_type):
required: **true**
(undocumented)
/// - [`requested_limit(i64)`](crate::operation::update_usage_limits::builders::UpdateUsageLimitsFluentBuilder::requested_limit) / [`set_requested_limit(Option)`](crate::operation::update_usage_limits::builders::UpdateUsageLimitsFluentBuilder::set_requested_limit):
required: **true**
(undocumented)
/// - [`justification(impl Into)`](crate::operation::update_usage_limits::builders::UpdateUsageLimitsFluentBuilder::justification) / [`set_justification(Option)`](crate::operation::update_usage_limits::builders::UpdateUsageLimitsFluentBuilder::set_justification):
required: **false**
(undocumented)
+ /// - [`permanent_override(bool)`](crate::operation::update_usage_limits::builders::UpdateUsageLimitsFluentBuilder::permanent_override) / [`set_permanent_override(Option)`](crate::operation::update_usage_limits::builders::UpdateUsageLimitsFluentBuilder::set_permanent_override):
required: **false**
(undocumented)
/// - On success, responds with /// [`UpdateUsageLimitsOutput`](crate::operation::update_usage_limits::UpdateUsageLimitsOutput) /// with field(s): diff --git a/crates/amzn-codewhisperer-client/src/operation/get_usage_limits.rs b/crates/amzn-codewhisperer-client/src/operation/get_usage_limits.rs index 633a99440b..b39c247afd 100644 --- a/crates/amzn-codewhisperer-client/src/operation/get_usage_limits.rs +++ b/crates/amzn-codewhisperer-client/src/operation/get_usage_limits.rs @@ -209,16 +209,21 @@ impl ::aws_smithy_runtime_api::client::ser_de::SerializeRequest for GetUsageLimi query.push_kv("profileArn", &::aws_smithy_http::query::fmt_string(inner_1)); } } - if let ::std::option::Option::Some(inner_2) = &_input.resource_type { + if let ::std::option::Option::Some(inner_2) = &_input.origin { { - query.push_kv("resourceType", &::aws_smithy_http::query::fmt_string(inner_2)); + query.push_kv("origin", &::aws_smithy_http::query::fmt_string(inner_2)); } } - if let ::std::option::Option::Some(inner_3) = &_input.is_email_required { + if let ::std::option::Option::Some(inner_3) = &_input.resource_type { + { + query.push_kv("resourceType", &::aws_smithy_http::query::fmt_string(inner_3)); + } + } + if let ::std::option::Option::Some(inner_4) = &_input.is_email_required { { query.push_kv( "isEmailRequired", - ::aws_smithy_types::primitive::Encoder::from(*inner_3).encode(), + ::aws_smithy_types::primitive::Encoder::from(*inner_4).encode(), ); } } diff --git a/crates/amzn-codewhisperer-client/src/operation/get_usage_limits/_get_usage_limits_input.rs b/crates/amzn-codewhisperer-client/src/operation/get_usage_limits/_get_usage_limits_input.rs index 2edfb389b2..2382a5d5d1 100644 --- a/crates/amzn-codewhisperer-client/src/operation/get_usage_limits/_get_usage_limits_input.rs +++ b/crates/amzn-codewhisperer-client/src/operation/get_usage_limits/_get_usage_limits_input.rs @@ -6,6 +6,8 @@ pub struct GetUsageLimitsInput { /// The ARN of the Q Developer profile. Required for enterprise customers, optional for Builder /// ID users. pub profile_arn: ::std::option::Option<::std::string::String>, + /// The origin of the client request to get limits for. + pub origin: ::std::option::Option, #[allow(missing_docs)] // documentation missing in model pub resource_type: ::std::option::Option, #[allow(missing_docs)] // documentation missing in model @@ -18,6 +20,11 @@ impl GetUsageLimitsInput { self.profile_arn.as_deref() } + /// The origin of the client request to get limits for. + pub fn origin(&self) -> ::std::option::Option<&crate::types::Origin> { + self.origin.as_ref() + } + #[allow(missing_docs)] // documentation missing in model pub fn resource_type(&self) -> ::std::option::Option<&crate::types::ResourceType> { self.resource_type.as_ref() @@ -41,6 +48,7 @@ impl GetUsageLimitsInput { #[non_exhaustive] pub struct GetUsageLimitsInputBuilder { pub(crate) profile_arn: ::std::option::Option<::std::string::String>, + pub(crate) origin: ::std::option::Option, pub(crate) resource_type: ::std::option::Option, pub(crate) is_email_required: ::std::option::Option, } @@ -65,6 +73,23 @@ impl GetUsageLimitsInputBuilder { &self.profile_arn } + /// The origin of the client request to get limits for. + pub fn origin(mut self, input: crate::types::Origin) -> Self { + self.origin = ::std::option::Option::Some(input); + self + } + + /// The origin of the client request to get limits for. + pub fn set_origin(mut self, input: ::std::option::Option) -> Self { + self.origin = input; + self + } + + /// The origin of the client request to get limits for. + pub fn get_origin(&self) -> &::std::option::Option { + &self.origin + } + #[allow(missing_docs)] // documentation missing in model pub fn resource_type(mut self, input: crate::types::ResourceType) -> Self { self.resource_type = ::std::option::Option::Some(input); @@ -109,6 +134,7 @@ impl GetUsageLimitsInputBuilder { > { ::std::result::Result::Ok(crate::operation::get_usage_limits::GetUsageLimitsInput { profile_arn: self.profile_arn, + origin: self.origin, resource_type: self.resource_type, is_email_required: self.is_email_required, }) diff --git a/crates/amzn-codewhisperer-client/src/operation/get_usage_limits/builders.rs b/crates/amzn-codewhisperer-client/src/operation/get_usage_limits/builders.rs index abf70c99cc..760be9771c 100644 --- a/crates/amzn-codewhisperer-client/src/operation/get_usage_limits/builders.rs +++ b/crates/amzn-codewhisperer-client/src/operation/get_usage_limits/builders.rs @@ -138,6 +138,23 @@ impl GetUsageLimitsFluentBuilder { self.inner.get_profile_arn() } + /// The origin of the client request to get limits for. + pub fn origin(mut self, input: crate::types::Origin) -> Self { + self.inner = self.inner.origin(input); + self + } + + /// The origin of the client request to get limits for. + pub fn set_origin(mut self, input: ::std::option::Option) -> Self { + self.inner = self.inner.set_origin(input); + self + } + + /// The origin of the client request to get limits for. + pub fn get_origin(&self) -> &::std::option::Option { + self.inner.get_origin() + } + #[allow(missing_docs)] // documentation missing in model pub fn resource_type(mut self, input: crate::types::ResourceType) -> Self { self.inner = self.inner.resource_type(input); diff --git a/crates/amzn-codewhisperer-client/src/operation/list_available_subscriptions/_list_available_subscriptions_output.rs b/crates/amzn-codewhisperer-client/src/operation/list_available_subscriptions/_list_available_subscriptions_output.rs index c466e880b9..d5eaf2407c 100644 --- a/crates/amzn-codewhisperer-client/src/operation/list_available_subscriptions/_list_available_subscriptions_output.rs +++ b/crates/amzn-codewhisperer-client/src/operation/list_available_subscriptions/_list_available_subscriptions_output.rs @@ -5,6 +5,8 @@ pub struct ListAvailableSubscriptionsOutput { #[allow(missing_docs)] // documentation missing in model pub subscription_plans: ::std::vec::Vec, + #[allow(missing_docs)] // documentation missing in model + pub disclaimer: ::std::option::Option<::std::vec::Vec<::std::string::String>>, _request_id: Option, } impl ListAvailableSubscriptionsOutput { @@ -13,6 +15,13 @@ impl ListAvailableSubscriptionsOutput { use std::ops::Deref; self.subscription_plans.deref() } + + #[allow(missing_docs)] // documentation missing in model + /// If no value was sent for this field, a default will be set. If you want to determine if no + /// value was sent, use `.disclaimer.is_none()`. + pub fn disclaimer(&self) -> &[::std::string::String] { + self.disclaimer.as_deref().unwrap_or_default() + } } impl ::aws_types::request_id::RequestId for ListAvailableSubscriptionsOutput { fn request_id(&self) -> Option<&str> { @@ -34,6 +43,7 @@ impl ListAvailableSubscriptionsOutput { #[non_exhaustive] pub struct ListAvailableSubscriptionsOutputBuilder { pub(crate) subscription_plans: ::std::option::Option<::std::vec::Vec>, + pub(crate) disclaimer: ::std::option::Option<::std::vec::Vec<::std::string::String>>, _request_id: Option, } impl ListAvailableSubscriptionsOutputBuilder { @@ -62,6 +72,27 @@ impl ListAvailableSubscriptionsOutputBuilder { &self.subscription_plans } + /// Appends an item to `disclaimer`. + /// + /// To override the contents of this collection use [`set_disclaimer`](Self::set_disclaimer). + pub fn disclaimer(mut self, input: impl ::std::convert::Into<::std::string::String>) -> Self { + let mut v = self.disclaimer.unwrap_or_default(); + v.push(input.into()); + self.disclaimer = ::std::option::Option::Some(v); + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn set_disclaimer(mut self, input: ::std::option::Option<::std::vec::Vec<::std::string::String>>) -> Self { + self.disclaimer = input; + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn get_disclaimer(&self) -> &::std::option::Option<::std::vec::Vec<::std::string::String>> { + &self.disclaimer + } + pub(crate) fn _request_id(mut self, request_id: impl Into) -> Self { self._request_id = Some(request_id.into()); self @@ -89,6 +120,7 @@ impl ListAvailableSubscriptionsOutputBuilder { "subscription_plans was not specified but it is required when building ListAvailableSubscriptionsOutput", ) })?, + disclaimer: self.disclaimer, _request_id: self._request_id, }) } diff --git a/crates/amzn-codewhisperer-client/src/operation/update_usage_limits/_update_usage_limits_input.rs b/crates/amzn-codewhisperer-client/src/operation/update_usage_limits/_update_usage_limits_input.rs index effbaf6544..2334773c43 100644 --- a/crates/amzn-codewhisperer-client/src/operation/update_usage_limits/_update_usage_limits_input.rs +++ b/crates/amzn-codewhisperer-client/src/operation/update_usage_limits/_update_usage_limits_input.rs @@ -15,6 +15,8 @@ pub struct UpdateUsageLimitsInput { pub requested_limit: ::std::option::Option, #[allow(missing_docs)] // documentation missing in model pub justification: ::std::option::Option<::std::string::String>, + #[allow(missing_docs)] // documentation missing in model + pub permanent_override: ::std::option::Option, } impl UpdateUsageLimitsInput { #[allow(missing_docs)] // documentation missing in model @@ -46,6 +48,11 @@ impl UpdateUsageLimitsInput { pub fn justification(&self) -> ::std::option::Option<&str> { self.justification.as_deref() } + + #[allow(missing_docs)] // documentation missing in model + pub fn permanent_override(&self) -> ::std::option::Option { + self.permanent_override + } } impl UpdateUsageLimitsInput { /// Creates a new builder-style object to manufacture @@ -66,6 +73,7 @@ pub struct UpdateUsageLimitsInputBuilder { pub(crate) feature_type: ::std::option::Option, pub(crate) requested_limit: ::std::option::Option, pub(crate) justification: ::std::option::Option<::std::string::String>, + pub(crate) permanent_override: ::std::option::Option, } impl UpdateUsageLimitsInputBuilder { #[allow(missing_docs)] // documentation missing in model @@ -173,6 +181,23 @@ impl UpdateUsageLimitsInputBuilder { &self.justification } + #[allow(missing_docs)] // documentation missing in model + pub fn permanent_override(mut self, input: bool) -> Self { + self.permanent_override = ::std::option::Option::Some(input); + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn set_permanent_override(mut self, input: ::std::option::Option) -> Self { + self.permanent_override = input; + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn get_permanent_override(&self) -> &::std::option::Option { + &self.permanent_override + } + /// Consumes the builder and constructs a /// [`UpdateUsageLimitsInput`](crate::operation::update_usage_limits::UpdateUsageLimitsInput). pub fn build( @@ -188,6 +213,7 @@ impl UpdateUsageLimitsInputBuilder { feature_type: self.feature_type, requested_limit: self.requested_limit, justification: self.justification, + permanent_override: self.permanent_override, }) } } diff --git a/crates/amzn-codewhisperer-client/src/operation/update_usage_limits/builders.rs b/crates/amzn-codewhisperer-client/src/operation/update_usage_limits/builders.rs index 60a01f4924..e5687b7ea3 100644 --- a/crates/amzn-codewhisperer-client/src/operation/update_usage_limits/builders.rs +++ b/crates/amzn-codewhisperer-client/src/operation/update_usage_limits/builders.rs @@ -219,4 +219,21 @@ impl UpdateUsageLimitsFluentBuilder { pub fn get_justification(&self) -> &::std::option::Option<::std::string::String> { self.inner.get_justification() } + + #[allow(missing_docs)] // documentation missing in model + pub fn permanent_override(mut self, input: bool) -> Self { + self.inner = self.inner.permanent_override(input); + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn set_permanent_override(mut self, input: ::std::option::Option) -> Self { + self.inner = self.inner.set_permanent_override(input); + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn get_permanent_override(&self) -> &::std::option::Option { + self.inner.get_permanent_override() + } } diff --git a/crates/amzn-codewhisperer-client/src/protocol_serde.rs b/crates/amzn-codewhisperer-client/src/protocol_serde.rs index 0ea7d595c5..049f4a5afa 100644 --- a/crates/amzn-codewhisperer-client/src/protocol_serde.rs +++ b/crates/amzn-codewhisperer-client/src/protocol_serde.rs @@ -212,6 +212,8 @@ pub(crate) mod shape_conversation_state; pub(crate) mod shape_customizations; +pub(crate) mod shape_disclaimer_list; + pub(crate) mod shape_editor_state; pub(crate) mod shape_event_list; diff --git a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_disclaimer_list.rs b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_disclaimer_list.rs new file mode 100644 index 0000000000..e07984215b --- /dev/null +++ b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_disclaimer_list.rs @@ -0,0 +1,42 @@ +// Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT. +pub(crate) fn de_disclaimer_list<'a, I>( + tokens: &mut ::std::iter::Peekable, +) -> ::std::result::Result< + Option<::std::vec::Vec<::std::string::String>>, + ::aws_smithy_json::deserialize::error::DeserializeError, +> +where + I: Iterator< + Item = Result< + ::aws_smithy_json::deserialize::Token<'a>, + ::aws_smithy_json::deserialize::error::DeserializeError, + >, + >, +{ + match tokens.next().transpose()? { + Some(::aws_smithy_json::deserialize::Token::ValueNull { .. }) => Ok(None), + Some(::aws_smithy_json::deserialize::Token::StartArray { .. }) => { + let mut items = Vec::new(); + loop { + match tokens.peek() { + Some(Ok(::aws_smithy_json::deserialize::Token::EndArray { .. })) => { + tokens.next().transpose().unwrap(); + break; + }, + _ => { + let value = ::aws_smithy_json::deserialize::token::expect_string_or_null(tokens.next())? + .map(|s| s.to_unescaped().map(|u| u.into_owned())) + .transpose()?; + if let Some(value) = value { + items.push(value); + } + }, + } + } + Ok(Some(items)) + }, + _ => Err(::aws_smithy_json::deserialize::error::DeserializeError::custom( + "expected start array or null", + )), + } +} diff --git a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_get_usage_limits_input.rs b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_get_usage_limits_input.rs index d29220ed3b..9444b6ee21 100644 --- a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_get_usage_limits_input.rs +++ b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_get_usage_limits_input.rs @@ -6,11 +6,14 @@ pub fn ser_get_usage_limits_input_input( if let Some(var_1) = &input.profile_arn { object.key("profileArn").string(var_1.as_str()); } - if let Some(var_2) = &input.resource_type { - object.key("resourceType").string(var_2.as_str()); + if let Some(var_2) = &input.origin { + object.key("origin").string(var_2.as_str()); } - if let Some(var_3) = &input.is_email_required { - object.key("isEmailRequired").boolean(*var_3); + if let Some(var_3) = &input.resource_type { + object.key("resourceType").string(var_3.as_str()); + } + if let Some(var_4) = &input.is_email_required { + object.key("isEmailRequired").boolean(*var_4); } Ok(()) } diff --git a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_list_available_subscriptions.rs b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_list_available_subscriptions.rs index 0a9d806be9..94affbd496 100644 --- a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_list_available_subscriptions.rs +++ b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_list_available_subscriptions.rs @@ -175,6 +175,11 @@ pub(crate) fn de_list_available_subscriptions( crate::protocol_serde::shape_subscription_plan_list::de_subscription_plan_list(tokens)?, ); }, + "disclaimer" => { + builder = builder.set_disclaimer(crate::protocol_serde::shape_disclaimer_list::de_disclaimer_list( + tokens, + )?); + }, _ => ::aws_smithy_json::deserialize::token::skip_value(tokens)?, }, other => { diff --git a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_model.rs b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_model.rs index 270675b377..f9bfb3c7b8 100644 --- a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_model.rs +++ b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_model.rs @@ -51,6 +51,11 @@ where crate::protocol_serde::shape_supported_input_types_list::de_supported_input_types_list(tokens)?, ); }, + "supportsPromptCache" => { + builder = builder.set_supports_prompt_cache( + ::aws_smithy_json::deserialize::token::expect_bool_or_null(tokens.next())?, + ); + }, _ => ::aws_smithy_json::deserialize::token::skip_value(tokens)?, } }, diff --git a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_subscription_info.rs b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_subscription_info.rs index 0eeeb1a6cd..0e836d08e9 100644 --- a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_subscription_info.rs +++ b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_subscription_info.rs @@ -33,14 +33,41 @@ where .transpose()?, ); }, - "upgradeCapable" => { - builder = builder.set_upgrade_capable( - ::aws_smithy_json::deserialize::token::expect_bool_or_null(tokens.next())?, + "upgradeCapability" => { + builder = builder.set_upgrade_capability( + ::aws_smithy_json::deserialize::token::expect_string_or_null(tokens.next())? + .map(|s| { + s.to_unescaped() + .map(|u| crate::types::UpgradeCapability::from(u.as_ref())) + }) + .transpose()?, ); }, - "overageCapable" => { - builder = builder.set_overage_capable( - ::aws_smithy_json::deserialize::token::expect_bool_or_null(tokens.next())?, + "overageCapability" => { + builder = builder.set_overage_capability( + ::aws_smithy_json::deserialize::token::expect_string_or_null(tokens.next())? + .map(|s| { + s.to_unescaped() + .map(|u| crate::types::OverageCapability::from(u.as_ref())) + }) + .transpose()?, + ); + }, + "subscriptionManagementTarget" => { + builder = builder.set_subscription_management_target( + ::aws_smithy_json::deserialize::token::expect_string_or_null(tokens.next())? + .map(|s| { + s.to_unescaped() + .map(|u| crate::types::SubscriptionManagementTarget::from(u.as_ref())) + }) + .transpose()?, + ); + }, + "subscriptionTitle" => { + builder = builder.set_subscription_title( + ::aws_smithy_json::deserialize::token::expect_string_or_null(tokens.next())? + .map(|s| s.to_unescaped().map(|u| u.into_owned())) + .transpose()?, ); }, _ => ::aws_smithy_json::deserialize::token::skip_value(tokens)?, diff --git a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_subscription_plan_description.rs b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_subscription_plan_description.rs index eca9964f89..48b9719c17 100644 --- a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_subscription_plan_description.rs +++ b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_subscription_plan_description.rs @@ -41,6 +41,13 @@ where builder = builder .set_features(crate::protocol_serde::shape_feature_list::de_feature_list(tokens)?); }, + "billingInterval" => { + builder = builder.set_billing_interval( + ::aws_smithy_json::deserialize::token::expect_string_or_null(tokens.next())? + .map(|s| s.to_unescaped().map(|u| u.into_owned())) + .transpose()?, + ); + }, _ => ::aws_smithy_json::deserialize::token::skip_value(tokens)?, } }, diff --git a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_update_usage_limits_input.rs b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_update_usage_limits_input.rs index 0bccde026c..d01ac1b9b2 100644 --- a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_update_usage_limits_input.rs +++ b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_update_usage_limits_input.rs @@ -24,5 +24,8 @@ pub fn ser_update_usage_limits_input_input( if let Some(var_6) = &input.justification { object.key("justification").string(var_6.as_str()); } + if let Some(var_7) = &input.permanent_override { + object.key("permanentOverride").boolean(*var_7); + } Ok(()) } diff --git a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_user_context.rs b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_user_context.rs index d3fb89d32b..dff95252f2 100644 --- a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_user_context.rs +++ b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_user_context.rs @@ -18,5 +18,11 @@ pub fn ser_user_context( if let Some(var_2) = &input.ide_version { object.key("ideVersion").string(var_2.as_str()); } + if let Some(var_3) = &input.plugin_version { + object.key("pluginVersion").string(var_3.as_str()); + } + if let Some(var_4) = &input.lsp_version { + object.key("lspVersion").string(var_4.as_str()); + } Ok(()) } diff --git a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_user_trigger_decision_event.rs b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_user_trigger_decision_event.rs index c542cb49d3..6e47ea70f6 100644 --- a/crates/amzn-codewhisperer-client/src/protocol_serde/shape_user_trigger_decision_event.rs +++ b/crates/amzn-codewhisperer-client/src/protocol_serde/shape_user_trigger_decision_event.rs @@ -116,5 +116,8 @@ pub fn ser_user_trigger_decision_event( ::aws_smithy_types::Number::NegInt((input.streak_length).into()), ); } + if let Some(var_13) = &input.suggestion_type { + object.key("suggestionType").string(var_13.as_str()); + } Ok(()) } diff --git a/crates/amzn-codewhisperer-client/src/serde_util.rs b/crates/amzn-codewhisperer-client/src/serde_util.rs index 0734a296e0..4e7308f097 100644 --- a/crates/amzn-codewhisperer-client/src/serde_util.rs +++ b/crates/amzn-codewhisperer-client/src/serde_util.rs @@ -463,11 +463,19 @@ pub(crate) fn subscription_info_correct_errors( if builder.r#type.is_none() { builder.r#type = "no value was set".parse::().ok() } - if builder.upgrade_capable.is_none() { - builder.upgrade_capable = Some(Default::default()) + if builder.upgrade_capability.is_none() { + builder.upgrade_capability = "no value was set".parse::().ok() } - if builder.overage_capable.is_none() { - builder.overage_capable = Some(Default::default()) + if builder.overage_capability.is_none() { + builder.overage_capability = "no value was set".parse::().ok() + } + if builder.subscription_management_target.is_none() { + builder.subscription_management_target = "no value was set" + .parse::() + .ok() + } + if builder.subscription_title.is_none() { + builder.subscription_title = Some(Default::default()) } builder } diff --git a/crates/amzn-codewhisperer-client/src/types.rs b/crates/amzn-codewhisperer-client/src/types.rs index 64f57e8b59..51ef7a0c0c 100644 --- a/crates/amzn-codewhisperer-client/src/types.rs +++ b/crates/amzn-codewhisperer-client/src/types.rs @@ -109,6 +109,7 @@ pub use crate::types::_opt_in_feature_toggle::OptInFeatureToggle; pub use crate::types::_opt_in_features::OptInFeatures; pub use crate::types::_opt_out_preference::OptOutPreference; pub use crate::types::_origin::Origin; +pub use crate::types::_overage_capability::OverageCapability; pub use crate::types::_overage_configuration::OverageConfiguration; pub use crate::types::_overage_status::OverageStatus; pub use crate::types::_package_info::PackageInfo; @@ -139,6 +140,7 @@ pub use crate::types::_shell_state::ShellState; pub use crate::types::_span::Span; pub use crate::types::_sso_identity_details::SsoIdentityDetails; pub use crate::types::_subscription_info::SubscriptionInfo; +pub use crate::types::_subscription_management_target::SubscriptionManagementTarget; pub use crate::types::_subscription_name::SubscriptionName; pub use crate::types::_subscription_plan::SubscriptionPlan; pub use crate::types::_subscription_plan_description::SubscriptionPlanDescription; @@ -147,6 +149,7 @@ pub use crate::types::_subscription_status::SubscriptionStatus; pub use crate::types::_subscription_type::SubscriptionType; pub use crate::types::_suggested_fix::SuggestedFix; pub use crate::types::_suggestion_state::SuggestionState; +pub use crate::types::_suggestion_type::SuggestionType; pub use crate::types::_supplemental_context::SupplementalContext; pub use crate::types::_supplemental_context_metadata::SupplementalContextMetadata; pub use crate::types::_supplemental_context_type::SupplementalContextType; @@ -199,6 +202,7 @@ pub use crate::types::_transformation_type::TransformationType; pub use crate::types::_transformation_upload_artifact_type::TransformationUploadArtifactType; pub use crate::types::_transformation_upload_context::TransformationUploadContext; pub use crate::types::_transformation_user_action_status::TransformationUserActionStatus; +pub use crate::types::_upgrade_capability::UpgradeCapability; pub use crate::types::_upload_context::UploadContext; pub use crate::types::_upload_intent::UploadIntent; pub use crate::types::_usage_breakdown::UsageBreakdown; @@ -440,6 +444,8 @@ mod _opt_out_preference; mod _origin; +mod _overage_capability; + mod _overage_configuration; mod _overage_status; @@ -500,6 +506,8 @@ mod _sso_identity_details; mod _subscription_info; +mod _subscription_management_target; + mod _subscription_name; mod _subscription_plan; @@ -516,6 +524,8 @@ mod _suggested_fix; mod _suggestion_state; +mod _suggestion_type; + mod _supplemental_context; mod _supplemental_context_metadata; @@ -620,6 +630,8 @@ mod _transformation_upload_context; mod _transformation_user_action_status; +mod _upgrade_capability; + mod _upload_context; mod _upload_intent; diff --git a/crates/amzn-codewhisperer-client/src/types/_access_denied_exception_reason.rs b/crates/amzn-codewhisperer-client/src/types/_access_denied_exception_reason.rs index 3c0f71561f..a4d9001996 100644 --- a/crates/amzn-codewhisperer-client/src/types/_access_denied_exception_reason.rs +++ b/crates/amzn-codewhisperer-client/src/types/_access_denied_exception_reason.rs @@ -12,6 +12,7 @@ /// ```text /// # let accessdeniedexceptionreason = unimplemented!(); /// match accessdeniedexceptionreason { +/// AccessDeniedExceptionReason::FeatureNotSupported => { /* ... */ }, /// AccessDeniedExceptionReason::TemporarilySuspended => { /* ... */ }, /// AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess => { /* ... */ }, /// AccessDeniedExceptionReason::UnauthorizedWorkspaceContextFeatureAccess => { /* ... */ }, @@ -49,6 +50,8 @@ ::std::hash::Hash, )] pub enum AccessDeniedExceptionReason { + #[allow(missing_docs)] // documentation missing in model + FeatureNotSupported, #[allow(missing_docs)] // documentation missing in model TemporarilySuspended, #[allow(missing_docs)] // documentation missing in model @@ -64,6 +67,7 @@ pub enum AccessDeniedExceptionReason { impl ::std::convert::From<&str> for AccessDeniedExceptionReason { fn from(s: &str) -> Self { match s { + "FEATURE_NOT_SUPPORTED" => AccessDeniedExceptionReason::FeatureNotSupported, "TEMPORARILY_SUSPENDED" => AccessDeniedExceptionReason::TemporarilySuspended, "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS" => { AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess @@ -88,6 +92,7 @@ impl AccessDeniedExceptionReason { /// Returns the `&str` value of the enum member. pub fn as_str(&self) -> &str { match self { + AccessDeniedExceptionReason::FeatureNotSupported => "FEATURE_NOT_SUPPORTED", AccessDeniedExceptionReason::TemporarilySuspended => "TEMPORARILY_SUSPENDED", AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess => { "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS" @@ -102,6 +107,7 @@ impl AccessDeniedExceptionReason { /// Returns all the `&str` representations of the enum members. pub const fn values() -> &'static [&'static str] { &[ + "FEATURE_NOT_SUPPORTED", "TEMPORARILY_SUSPENDED", "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS", "UNAUTHORIZED_WORKSPACE_CONTEXT_FEATURE_ACCESS", @@ -128,6 +134,7 @@ impl AccessDeniedExceptionReason { impl ::std::fmt::Display for AccessDeniedExceptionReason { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { match self { + AccessDeniedExceptionReason::FeatureNotSupported => write!(f, "FEATURE_NOT_SUPPORTED"), AccessDeniedExceptionReason::TemporarilySuspended => write!(f, "TEMPORARILY_SUSPENDED"), AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess => { write!(f, "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS") diff --git a/crates/amzn-codewhisperer-client/src/types/_chat_message_interaction_type.rs b/crates/amzn-codewhisperer-client/src/types/_chat_message_interaction_type.rs index 7c0f5d657b..cacd77360c 100644 --- a/crates/amzn-codewhisperer-client/src/types/_chat_message_interaction_type.rs +++ b/crates/amzn-codewhisperer-client/src/types/_chat_message_interaction_type.rs @@ -12,6 +12,7 @@ /// ```text /// # let chatmessageinteractiontype = unimplemented!(); /// match chatmessageinteractiontype { +/// ChatMessageInteractionType::AgenticCodeAccepted => { /* ... */ }, /// ChatMessageInteractionType::ClickBodyLink => { /* ... */ }, /// ChatMessageInteractionType::ClickFollowUp => { /* ... */ }, /// ChatMessageInteractionType::ClickLink => { /* ... */ }, @@ -55,6 +56,8 @@ ::std::hash::Hash, )] pub enum ChatMessageInteractionType { + #[allow(missing_docs)] // documentation missing in model + AgenticCodeAccepted, #[allow(missing_docs)] // documentation missing in model ClickBodyLink, #[allow(missing_docs)] // documentation missing in model @@ -82,6 +85,7 @@ pub enum ChatMessageInteractionType { impl ::std::convert::From<&str> for ChatMessageInteractionType { fn from(s: &str) -> Self { match s { + "AGENTIC_CODE_ACCEPTED" => ChatMessageInteractionType::AgenticCodeAccepted, "CLICK_BODY_LINK" => ChatMessageInteractionType::ClickBodyLink, "CLICK_FOLLOW_UP" => ChatMessageInteractionType::ClickFollowUp, "CLICK_LINK" => ChatMessageInteractionType::ClickLink, @@ -108,6 +112,7 @@ impl ChatMessageInteractionType { /// Returns the `&str` value of the enum member. pub fn as_str(&self) -> &str { match self { + ChatMessageInteractionType::AgenticCodeAccepted => "AGENTIC_CODE_ACCEPTED", ChatMessageInteractionType::ClickBodyLink => "CLICK_BODY_LINK", ChatMessageInteractionType::ClickFollowUp => "CLICK_FOLLOW_UP", ChatMessageInteractionType::ClickLink => "CLICK_LINK", @@ -124,6 +129,7 @@ impl ChatMessageInteractionType { /// Returns all the `&str` representations of the enum members. pub const fn values() -> &'static [&'static str] { &[ + "AGENTIC_CODE_ACCEPTED", "CLICK_BODY_LINK", "CLICK_FOLLOW_UP", "CLICK_LINK", @@ -156,6 +162,7 @@ impl ChatMessageInteractionType { impl ::std::fmt::Display for ChatMessageInteractionType { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { match self { + ChatMessageInteractionType::AgenticCodeAccepted => write!(f, "AGENTIC_CODE_ACCEPTED"), ChatMessageInteractionType::ClickBodyLink => write!(f, "CLICK_BODY_LINK"), ChatMessageInteractionType::ClickFollowUp => write!(f, "CLICK_FOLLOW_UP"), ChatMessageInteractionType::ClickLink => write!(f, "CLICK_LINK"), diff --git a/crates/amzn-codewhisperer-client/src/types/_model.rs b/crates/amzn-codewhisperer-client/src/types/_model.rs index 7ccaf47967..35a7879c4b 100644 --- a/crates/amzn-codewhisperer-client/src/types/_model.rs +++ b/crates/amzn-codewhisperer-client/src/types/_model.rs @@ -13,6 +13,8 @@ pub struct Model { pub token_limits: ::std::option::Option, /// List of input types supported by this model pub supported_input_types: ::std::option::Option<::std::vec::Vec>, + /// Whether the model supports prompt caching + pub supports_prompt_cache: ::std::option::Option, } impl Model { /// Unique identifier for the model @@ -43,6 +45,11 @@ impl Model { pub fn supported_input_types(&self) -> &[crate::types::InputType] { self.supported_input_types.as_deref().unwrap_or_default() } + + /// Whether the model supports prompt caching + pub fn supports_prompt_cache(&self) -> ::std::option::Option { + self.supports_prompt_cache + } } impl Model { /// Creates a new builder-style object to manufacture [`Model`](crate::types::Model). @@ -60,6 +67,7 @@ pub struct ModelBuilder { pub(crate) description: ::std::option::Option<::std::string::String>, pub(crate) token_limits: ::std::option::Option, pub(crate) supported_input_types: ::std::option::Option<::std::vec::Vec>, + pub(crate) supports_prompt_cache: ::std::option::Option, } impl ModelBuilder { /// Unique identifier for the model @@ -158,6 +166,23 @@ impl ModelBuilder { &self.supported_input_types } + /// Whether the model supports prompt caching + pub fn supports_prompt_cache(mut self, input: bool) -> Self { + self.supports_prompt_cache = ::std::option::Option::Some(input); + self + } + + /// Whether the model supports prompt caching + pub fn set_supports_prompt_cache(mut self, input: ::std::option::Option) -> Self { + self.supports_prompt_cache = input; + self + } + + /// Whether the model supports prompt caching + pub fn get_supports_prompt_cache(&self) -> &::std::option::Option { + &self.supports_prompt_cache + } + /// Consumes the builder and constructs a [`Model`](crate::types::Model). /// This method will fail if any of the following fields are not set: /// - [`model_id`](crate::types::builders::ModelBuilder::model_id) @@ -173,6 +198,7 @@ impl ModelBuilder { description: self.description, token_limits: self.token_limits, supported_input_types: self.supported_input_types, + supports_prompt_cache: self.supports_prompt_cache, }) } } diff --git a/crates/amzn-codewhisperer-client/src/types/_origin.rs b/crates/amzn-codewhisperer-client/src/types/_origin.rs index 4984c919de..be6c84095a 100644 --- a/crates/amzn-codewhisperer-client/src/types/_origin.rs +++ b/crates/amzn-codewhisperer-client/src/types/_origin.rs @@ -19,6 +19,7 @@ /// Origin::Documentation => { /* ... */ }, /// Origin::Gitlab => { /* ... */ }, /// Origin::Ide => { /* ... */ }, +/// Origin::InlineChat => { /* ... */ }, /// Origin::Marketing => { /* ... */ }, /// Origin::Md => { /* ... */ }, /// Origin::MdCe => { /* ... */ }, @@ -28,6 +29,7 @@ /// Origin::QDevBext => { /* ... */ }, /// Origin::SageMaker => { /* ... */ }, /// Origin::ServiceInternal => { /* ... */ }, +/// Origin::SmAiStudioIde => { /* ... */ }, /// Origin::UnifiedSearch => { /* ... */ }, /// Origin::UnknownValue => { /* ... */ }, /// other @ _ if other.as_str() == "NewFeature" => { /* handles a case for `NewFeature` */ }, @@ -80,6 +82,8 @@ pub enum Origin { Gitlab, /// Any IDE caller. Ide, + /// Q Developer Inline Chat. + InlineChat, /// AWS Marketing Website (https://aws.amazon.com) Marketing, /// MD. @@ -99,6 +103,8 @@ pub enum Origin { /// Internal Service Traffic (Integ Tests, Canaries, etc.). This is the default when no Origin /// header present in request. ServiceInternal, + /// SageMaker AI Studio IDE Chat + SmAiStudioIde, /// Unified Search in AWS Management Console (https://.console.aws.amazon.com) UnifiedSearch, /// Origin header is not set. @@ -121,6 +127,7 @@ impl ::std::convert::From<&str> for Origin { "DOCUMENTATION" => Origin::Documentation, "GITLAB" => Origin::Gitlab, "IDE" => Origin::Ide, + "INLINE_CHAT" => Origin::InlineChat, "MARKETING" => Origin::Marketing, "MD" => Origin::Md, "MD_CE" => Origin::MdCe, @@ -130,6 +137,7 @@ impl ::std::convert::From<&str> for Origin { "Q_DEV_BEXT" => Origin::QDevBext, "SAGE_MAKER" => Origin::SageMaker, "SERVICE_INTERNAL" => Origin::ServiceInternal, + "SM_AI_STUDIO_IDE" => Origin::SmAiStudioIde, "UNIFIED_SEARCH" => Origin::UnifiedSearch, "UNKNOWN" => Origin::UnknownValue, other => Origin::Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue( @@ -156,6 +164,7 @@ impl Origin { Origin::Documentation => "DOCUMENTATION", Origin::Gitlab => "GITLAB", Origin::Ide => "IDE", + Origin::InlineChat => "INLINE_CHAT", Origin::Marketing => "MARKETING", Origin::Md => "MD", Origin::MdCe => "MD_CE", @@ -165,6 +174,7 @@ impl Origin { Origin::QDevBext => "Q_DEV_BEXT", Origin::SageMaker => "SAGE_MAKER", Origin::ServiceInternal => "SERVICE_INTERNAL", + Origin::SmAiStudioIde => "SM_AI_STUDIO_IDE", Origin::UnifiedSearch => "UNIFIED_SEARCH", Origin::UnknownValue => "UNKNOWN", Origin::Unknown(value) => value.as_str(), @@ -181,6 +191,7 @@ impl Origin { "DOCUMENTATION", "GITLAB", "IDE", + "INLINE_CHAT", "MARKETING", "MD", "MD_CE", @@ -190,6 +201,7 @@ impl Origin { "Q_DEV_BEXT", "SAGE_MAKER", "SERVICE_INTERNAL", + "SM_AI_STUDIO_IDE", "UNIFIED_SEARCH", "UNKNOWN", ] @@ -222,6 +234,7 @@ impl ::std::fmt::Display for Origin { Origin::Documentation => write!(f, "DOCUMENTATION"), Origin::Gitlab => write!(f, "GITLAB"), Origin::Ide => write!(f, "IDE"), + Origin::InlineChat => write!(f, "INLINE_CHAT"), Origin::Marketing => write!(f, "MARKETING"), Origin::Md => write!(f, "MD"), Origin::MdCe => write!(f, "MD_CE"), @@ -231,6 +244,7 @@ impl ::std::fmt::Display for Origin { Origin::QDevBext => write!(f, "Q_DEV_BEXT"), Origin::SageMaker => write!(f, "SAGE_MAKER"), Origin::ServiceInternal => write!(f, "SERVICE_INTERNAL"), + Origin::SmAiStudioIde => write!(f, "SM_AI_STUDIO_IDE"), Origin::UnifiedSearch => write!(f, "UNIFIED_SEARCH"), Origin::UnknownValue => write!(f, "UNKNOWN"), Origin::Unknown(value) => write!(f, "{}", value), diff --git a/crates/amzn-codewhisperer-client/src/types/_overage_capability.rs b/crates/amzn-codewhisperer-client/src/types/_overage_capability.rs new file mode 100644 index 0000000000..e60be66778 --- /dev/null +++ b/crates/amzn-codewhisperer-client/src/types/_overage_capability.rs @@ -0,0 +1,118 @@ +// Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT. + +/// When writing a match expression against `OverageCapability`, it is important to ensure +/// your code is forward-compatible. That is, if a match arm handles a case for a +/// feature that is supported by the service but has not been represented as an enum +/// variant in a current version of SDK, your code should continue to work when you +/// upgrade SDK to a future version in which the enum does include a variant for that +/// feature. +/// +/// Here is an example of how you can make a match expression forward-compatible: +/// +/// ```text +/// # let overagecapability = unimplemented!(); +/// match overagecapability { +/// OverageCapability::OverageCapable => { /* ... */ }, +/// OverageCapability::OverageIncapable => { /* ... */ }, +/// other @ _ if other.as_str() == "NewFeature" => { /* handles a case for `NewFeature` */ }, +/// _ => { /* ... */ }, +/// } +/// ``` +/// The above code demonstrates that when `overagecapability` represents +/// `NewFeature`, the execution path will lead to the second last match arm, +/// even though the enum does not contain a variant `OverageCapability::NewFeature` +/// in the current version of SDK. The reason is that the variable `other`, +/// created by the `@` operator, is bound to +/// `OverageCapability::Unknown(UnknownVariantValue("NewFeature".to_owned()))` +/// and calling `as_str` on it yields `"NewFeature"`. +/// This match expression is forward-compatible when executed with a newer +/// version of SDK where the variant `OverageCapability::NewFeature` is defined. +/// Specifically, when `overagecapability` represents `NewFeature`, +/// the execution path will hit the second last match arm as before by virtue of +/// calling `as_str` on `OverageCapability::NewFeature` also yielding `"NewFeature"`. +/// +/// Explicitly matching on the `Unknown` variant should +/// be avoided for two reasons: +/// - The inner data `UnknownVariantValue` is opaque, and no further information can be extracted. +/// - It might inadvertently shadow other intended match arms. +#[allow(missing_docs)] // documentation missing in model +#[non_exhaustive] +#[derive( + ::std::clone::Clone, + ::std::cmp::Eq, + ::std::cmp::Ord, + ::std::cmp::PartialEq, + ::std::cmp::PartialOrd, + ::std::fmt::Debug, + ::std::hash::Hash, +)] +pub enum OverageCapability { + /// this user is able to use overages + OverageCapable, + /// this user is not able to use overages + OverageIncapable, + /// `Unknown` contains new variants that have been added since this code was generated. + #[deprecated( + note = "Don't directly match on `Unknown`. See the docs on this enum for the correct way to handle unknown variants." + )] + Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue), +} +impl ::std::convert::From<&str> for OverageCapability { + fn from(s: &str) -> Self { + match s { + "OVERAGE_CAPABLE" => OverageCapability::OverageCapable, + "OVERAGE_INCAPABLE" => OverageCapability::OverageIncapable, + other => OverageCapability::Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue( + other.to_owned(), + )), + } + } +} +impl ::std::str::FromStr for OverageCapability { + type Err = ::std::convert::Infallible; + + fn from_str(s: &str) -> ::std::result::Result::Err> { + ::std::result::Result::Ok(OverageCapability::from(s)) + } +} +impl OverageCapability { + /// Returns the `&str` value of the enum member. + pub fn as_str(&self) -> &str { + match self { + OverageCapability::OverageCapable => "OVERAGE_CAPABLE", + OverageCapability::OverageIncapable => "OVERAGE_INCAPABLE", + OverageCapability::Unknown(value) => value.as_str(), + } + } + + /// Returns all the `&str` representations of the enum members. + pub const fn values() -> &'static [&'static str] { + &["OVERAGE_CAPABLE", "OVERAGE_INCAPABLE"] + } +} +impl ::std::convert::AsRef for OverageCapability { + fn as_ref(&self) -> &str { + self.as_str() + } +} +impl OverageCapability { + /// Parses the enum value while disallowing unknown variants. + /// + /// Unknown variants will result in an error. + pub fn try_parse(value: &str) -> ::std::result::Result { + match Self::from(value) { + #[allow(deprecated)] + Self::Unknown(_) => ::std::result::Result::Err(crate::error::UnknownVariantError::new(value)), + known => Ok(known), + } + } +} +impl ::std::fmt::Display for OverageCapability { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + match self { + OverageCapability::OverageCapable => write!(f, "OVERAGE_CAPABLE"), + OverageCapability::OverageIncapable => write!(f, "OVERAGE_INCAPABLE"), + OverageCapability::Unknown(value) => write!(f, "{}", value), + } + } +} diff --git a/crates/amzn-codewhisperer-client/src/types/_subscription_info.rs b/crates/amzn-codewhisperer-client/src/types/_subscription_info.rs index 7437e6d86d..0ba8cfe9e2 100644 --- a/crates/amzn-codewhisperer-client/src/types/_subscription_info.rs +++ b/crates/amzn-codewhisperer-client/src/types/_subscription_info.rs @@ -6,9 +6,13 @@ pub struct SubscriptionInfo { /// Granted subscription type pub r#type: crate::types::SubscriptionType, /// Is this subscription upgradeable - pub upgrade_capable: bool, + pub upgrade_capability: crate::types::UpgradeCapability, /// Does this subscription support overages - pub overage_capable: bool, + pub overage_capability: crate::types::OverageCapability, + /// Where should the user be redirected for subscription management + pub subscription_management_target: crate::types::SubscriptionManagementTarget, + /// human friendly subscription title + pub subscription_title: ::std::string::String, } impl SubscriptionInfo { /// Granted subscription type @@ -17,13 +21,24 @@ impl SubscriptionInfo { } /// Is this subscription upgradeable - pub fn upgrade_capable(&self) -> bool { - self.upgrade_capable + pub fn upgrade_capability(&self) -> &crate::types::UpgradeCapability { + &self.upgrade_capability } /// Does this subscription support overages - pub fn overage_capable(&self) -> bool { - self.overage_capable + pub fn overage_capability(&self) -> &crate::types::OverageCapability { + &self.overage_capability + } + + /// Where should the user be redirected for subscription management + pub fn subscription_management_target(&self) -> &crate::types::SubscriptionManagementTarget { + &self.subscription_management_target + } + + /// human friendly subscription title + pub fn subscription_title(&self) -> &str { + use std::ops::Deref; + self.subscription_title.deref() } } impl SubscriptionInfo { @@ -39,8 +54,10 @@ impl SubscriptionInfo { #[non_exhaustive] pub struct SubscriptionInfoBuilder { pub(crate) r#type: ::std::option::Option, - pub(crate) upgrade_capable: ::std::option::Option, - pub(crate) overage_capable: ::std::option::Option, + pub(crate) upgrade_capability: ::std::option::Option, + pub(crate) overage_capability: ::std::option::Option, + pub(crate) subscription_management_target: ::std::option::Option, + pub(crate) subscription_title: ::std::option::Option<::std::string::String>, } impl SubscriptionInfoBuilder { /// Granted subscription type @@ -63,45 +80,88 @@ impl SubscriptionInfoBuilder { /// Is this subscription upgradeable /// This field is required. - pub fn upgrade_capable(mut self, input: bool) -> Self { - self.upgrade_capable = ::std::option::Option::Some(input); + pub fn upgrade_capability(mut self, input: crate::types::UpgradeCapability) -> Self { + self.upgrade_capability = ::std::option::Option::Some(input); self } /// Is this subscription upgradeable - pub fn set_upgrade_capable(mut self, input: ::std::option::Option) -> Self { - self.upgrade_capable = input; + pub fn set_upgrade_capability(mut self, input: ::std::option::Option) -> Self { + self.upgrade_capability = input; self } /// Is this subscription upgradeable - pub fn get_upgrade_capable(&self) -> &::std::option::Option { - &self.upgrade_capable + pub fn get_upgrade_capability(&self) -> &::std::option::Option { + &self.upgrade_capability } /// Does this subscription support overages /// This field is required. - pub fn overage_capable(mut self, input: bool) -> Self { - self.overage_capable = ::std::option::Option::Some(input); + pub fn overage_capability(mut self, input: crate::types::OverageCapability) -> Self { + self.overage_capability = ::std::option::Option::Some(input); self } /// Does this subscription support overages - pub fn set_overage_capable(mut self, input: ::std::option::Option) -> Self { - self.overage_capable = input; + pub fn set_overage_capability(mut self, input: ::std::option::Option) -> Self { + self.overage_capability = input; self } /// Does this subscription support overages - pub fn get_overage_capable(&self) -> &::std::option::Option { - &self.overage_capable + pub fn get_overage_capability(&self) -> &::std::option::Option { + &self.overage_capability + } + + /// Where should the user be redirected for subscription management + /// This field is required. + pub fn subscription_management_target(mut self, input: crate::types::SubscriptionManagementTarget) -> Self { + self.subscription_management_target = ::std::option::Option::Some(input); + self + } + + /// Where should the user be redirected for subscription management + pub fn set_subscription_management_target( + mut self, + input: ::std::option::Option, + ) -> Self { + self.subscription_management_target = input; + self + } + + /// Where should the user be redirected for subscription management + pub fn get_subscription_management_target( + &self, + ) -> &::std::option::Option { + &self.subscription_management_target + } + + /// human friendly subscription title + /// This field is required. + pub fn subscription_title(mut self, input: impl ::std::convert::Into<::std::string::String>) -> Self { + self.subscription_title = ::std::option::Option::Some(input.into()); + self + } + + /// human friendly subscription title + pub fn set_subscription_title(mut self, input: ::std::option::Option<::std::string::String>) -> Self { + self.subscription_title = input; + self + } + + /// human friendly subscription title + pub fn get_subscription_title(&self) -> &::std::option::Option<::std::string::String> { + &self.subscription_title } /// Consumes the builder and constructs a [`SubscriptionInfo`](crate::types::SubscriptionInfo). /// This method will fail if any of the following fields are not set: /// - [`r#type`](crate::types::builders::SubscriptionInfoBuilder::type) - /// - [`upgrade_capable`](crate::types::builders::SubscriptionInfoBuilder::upgrade_capable) - /// - [`overage_capable`](crate::types::builders::SubscriptionInfoBuilder::overage_capable) + /// - [`upgrade_capability`](crate::types::builders::SubscriptionInfoBuilder::upgrade_capability) + /// - [`overage_capability`](crate::types::builders::SubscriptionInfoBuilder::overage_capability) + /// - [`subscription_management_target`](crate::types::builders::SubscriptionInfoBuilder::subscription_management_target) + /// - [`subscription_title`](crate::types::builders::SubscriptionInfoBuilder::subscription_title) pub fn build( self, ) -> ::std::result::Result { @@ -112,16 +172,28 @@ impl SubscriptionInfoBuilder { "r#type was not specified but it is required when building SubscriptionInfo", ) })?, - upgrade_capable: self.upgrade_capable.ok_or_else(|| { + upgrade_capability: self.upgrade_capability.ok_or_else(|| { + ::aws_smithy_types::error::operation::BuildError::missing_field( + "upgrade_capability", + "upgrade_capability was not specified but it is required when building SubscriptionInfo", + ) + })?, + overage_capability: self.overage_capability.ok_or_else(|| { + ::aws_smithy_types::error::operation::BuildError::missing_field( + "overage_capability", + "overage_capability was not specified but it is required when building SubscriptionInfo", + ) + })?, + subscription_management_target: self.subscription_management_target.ok_or_else(|| { ::aws_smithy_types::error::operation::BuildError::missing_field( - "upgrade_capable", - "upgrade_capable was not specified but it is required when building SubscriptionInfo", + "subscription_management_target", + "subscription_management_target was not specified but it is required when building SubscriptionInfo", ) })?, - overage_capable: self.overage_capable.ok_or_else(|| { + subscription_title: self.subscription_title.ok_or_else(|| { ::aws_smithy_types::error::operation::BuildError::missing_field( - "overage_capable", - "overage_capable was not specified but it is required when building SubscriptionInfo", + "subscription_title", + "subscription_title was not specified but it is required when building SubscriptionInfo", ) })?, }) diff --git a/crates/amzn-codewhisperer-client/src/types/_subscription_management_target.rs b/crates/amzn-codewhisperer-client/src/types/_subscription_management_target.rs new file mode 100644 index 0000000000..0af005c417 --- /dev/null +++ b/crates/amzn-codewhisperer-client/src/types/_subscription_management_target.rs @@ -0,0 +1,118 @@ +// Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT. + +/// When writing a match expression against `SubscriptionManagementTarget`, it is important to +/// ensure your code is forward-compatible. That is, if a match arm handles a case for a +/// feature that is supported by the service but has not been represented as an enum +/// variant in a current version of SDK, your code should continue to work when you +/// upgrade SDK to a future version in which the enum does include a variant for that +/// feature. +/// +/// Here is an example of how you can make a match expression forward-compatible: +/// +/// ```text +/// # let subscriptionmanagementtarget = unimplemented!(); +/// match subscriptionmanagementtarget { +/// SubscriptionManagementTarget::Manage => { /* ... */ }, +/// SubscriptionManagementTarget::Purchase => { /* ... */ }, +/// other @ _ if other.as_str() == "NewFeature" => { /* handles a case for `NewFeature` */ }, +/// _ => { /* ... */ }, +/// } +/// ``` +/// The above code demonstrates that when `subscriptionmanagementtarget` represents +/// `NewFeature`, the execution path will lead to the second last match arm, +/// even though the enum does not contain a variant `SubscriptionManagementTarget::NewFeature` +/// in the current version of SDK. The reason is that the variable `other`, +/// created by the `@` operator, is bound to +/// `SubscriptionManagementTarget::Unknown(UnknownVariantValue("NewFeature".to_owned()))` +/// and calling `as_str` on it yields `"NewFeature"`. +/// This match expression is forward-compatible when executed with a newer +/// version of SDK where the variant `SubscriptionManagementTarget::NewFeature` is defined. +/// Specifically, when `subscriptionmanagementtarget` represents `NewFeature`, +/// the execution path will hit the second last match arm as before by virtue of +/// calling `as_str` on `SubscriptionManagementTarget::NewFeature` also yielding `"NewFeature"`. +/// +/// Explicitly matching on the `Unknown` variant should +/// be avoided for two reasons: +/// - The inner data `UnknownVariantValue` is opaque, and no further information can be extracted. +/// - It might inadvertently shadow other intended match arms. +#[allow(missing_docs)] // documentation missing in model +#[non_exhaustive] +#[derive( + ::std::clone::Clone, + ::std::cmp::Eq, + ::std::cmp::Ord, + ::std::cmp::PartialEq, + ::std::cmp::PartialOrd, + ::std::fmt::Debug, + ::std::hash::Hash, +)] +pub enum SubscriptionManagementTarget { + /// for managing existing subscriptions + Manage, + /// for checkout starting a subscription + Purchase, + /// `Unknown` contains new variants that have been added since this code was generated. + #[deprecated( + note = "Don't directly match on `Unknown`. See the docs on this enum for the correct way to handle unknown variants." + )] + Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue), +} +impl ::std::convert::From<&str> for SubscriptionManagementTarget { + fn from(s: &str) -> Self { + match s { + "MANAGE" => SubscriptionManagementTarget::Manage, + "PURCHASE" => SubscriptionManagementTarget::Purchase, + other => SubscriptionManagementTarget::Unknown( + crate::primitives::sealed_enum_unknown::UnknownVariantValue(other.to_owned()), + ), + } + } +} +impl ::std::str::FromStr for SubscriptionManagementTarget { + type Err = ::std::convert::Infallible; + + fn from_str(s: &str) -> ::std::result::Result::Err> { + ::std::result::Result::Ok(SubscriptionManagementTarget::from(s)) + } +} +impl SubscriptionManagementTarget { + /// Returns the `&str` value of the enum member. + pub fn as_str(&self) -> &str { + match self { + SubscriptionManagementTarget::Manage => "MANAGE", + SubscriptionManagementTarget::Purchase => "PURCHASE", + SubscriptionManagementTarget::Unknown(value) => value.as_str(), + } + } + + /// Returns all the `&str` representations of the enum members. + pub const fn values() -> &'static [&'static str] { + &["MANAGE", "PURCHASE"] + } +} +impl ::std::convert::AsRef for SubscriptionManagementTarget { + fn as_ref(&self) -> &str { + self.as_str() + } +} +impl SubscriptionManagementTarget { + /// Parses the enum value while disallowing unknown variants. + /// + /// Unknown variants will result in an error. + pub fn try_parse(value: &str) -> ::std::result::Result { + match Self::from(value) { + #[allow(deprecated)] + Self::Unknown(_) => ::std::result::Result::Err(crate::error::UnknownVariantError::new(value)), + known => Ok(known), + } + } +} +impl ::std::fmt::Display for SubscriptionManagementTarget { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + match self { + SubscriptionManagementTarget::Manage => write!(f, "MANAGE"), + SubscriptionManagementTarget::Purchase => write!(f, "PURCHASE"), + SubscriptionManagementTarget::Unknown(value) => write!(f, "{}", value), + } + } +} diff --git a/crates/amzn-codewhisperer-client/src/types/_subscription_plan_description.rs b/crates/amzn-codewhisperer-client/src/types/_subscription_plan_description.rs index 3fec5d44e4..3c691ff1cb 100644 --- a/crates/amzn-codewhisperer-client/src/types/_subscription_plan_description.rs +++ b/crates/amzn-codewhisperer-client/src/types/_subscription_plan_description.rs @@ -9,6 +9,8 @@ pub struct SubscriptionPlanDescription { pub feature_header: ::std::option::Option<::std::string::String>, #[allow(missing_docs)] // documentation missing in model pub features: ::std::option::Option<::std::vec::Vec<::std::string::String>>, + #[allow(missing_docs)] // documentation missing in model + pub billing_interval: ::std::option::Option<::std::string::String>, } impl SubscriptionPlanDescription { #[allow(missing_docs)] // documentation missing in model @@ -27,6 +29,11 @@ impl SubscriptionPlanDescription { pub fn features(&self) -> &[::std::string::String] { self.features.as_deref().unwrap_or_default() } + + #[allow(missing_docs)] // documentation missing in model + pub fn billing_interval(&self) -> ::std::option::Option<&str> { + self.billing_interval.as_deref() + } } impl SubscriptionPlanDescription { /// Creates a new builder-style object to manufacture @@ -43,6 +50,7 @@ pub struct SubscriptionPlanDescriptionBuilder { pub(crate) title: ::std::option::Option<::std::string::String>, pub(crate) feature_header: ::std::option::Option<::std::string::String>, pub(crate) features: ::std::option::Option<::std::vec::Vec<::std::string::String>>, + pub(crate) billing_interval: ::std::option::Option<::std::string::String>, } impl SubscriptionPlanDescriptionBuilder { #[allow(missing_docs)] // documentation missing in model @@ -100,6 +108,23 @@ impl SubscriptionPlanDescriptionBuilder { &self.features } + #[allow(missing_docs)] // documentation missing in model + pub fn billing_interval(mut self, input: impl ::std::convert::Into<::std::string::String>) -> Self { + self.billing_interval = ::std::option::Option::Some(input.into()); + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn set_billing_interval(mut self, input: ::std::option::Option<::std::string::String>) -> Self { + self.billing_interval = input; + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn get_billing_interval(&self) -> &::std::option::Option<::std::string::String> { + &self.billing_interval + } + /// Consumes the builder and constructs a /// [`SubscriptionPlanDescription`](crate::types::SubscriptionPlanDescription). pub fn build(self) -> crate::types::SubscriptionPlanDescription { @@ -107,6 +132,7 @@ impl SubscriptionPlanDescriptionBuilder { title: self.title, feature_header: self.feature_header, features: self.features, + billing_interval: self.billing_interval, } } } diff --git a/crates/amzn-codewhisperer-client/src/types/_suggestion_type.rs b/crates/amzn-codewhisperer-client/src/types/_suggestion_type.rs new file mode 100644 index 0000000000..08355739e4 --- /dev/null +++ b/crates/amzn-codewhisperer-client/src/types/_suggestion_type.rs @@ -0,0 +1,118 @@ +// Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT. + +/// When writing a match expression against `SuggestionType`, it is important to ensure +/// your code is forward-compatible. That is, if a match arm handles a case for a +/// feature that is supported by the service but has not been represented as an enum +/// variant in a current version of SDK, your code should continue to work when you +/// upgrade SDK to a future version in which the enum does include a variant for that +/// feature. +/// +/// Here is an example of how you can make a match expression forward-compatible: +/// +/// ```text +/// # let suggestiontype = unimplemented!(); +/// match suggestiontype { +/// SuggestionType::Completions => { /* ... */ }, +/// SuggestionType::Edits => { /* ... */ }, +/// other @ _ if other.as_str() == "NewFeature" => { /* handles a case for `NewFeature` */ }, +/// _ => { /* ... */ }, +/// } +/// ``` +/// The above code demonstrates that when `suggestiontype` represents +/// `NewFeature`, the execution path will lead to the second last match arm, +/// even though the enum does not contain a variant `SuggestionType::NewFeature` +/// in the current version of SDK. The reason is that the variable `other`, +/// created by the `@` operator, is bound to +/// `SuggestionType::Unknown(UnknownVariantValue("NewFeature".to_owned()))` +/// and calling `as_str` on it yields `"NewFeature"`. +/// This match expression is forward-compatible when executed with a newer +/// version of SDK where the variant `SuggestionType::NewFeature` is defined. +/// Specifically, when `suggestiontype` represents `NewFeature`, +/// the execution path will hit the second last match arm as before by virtue of +/// calling `as_str` on `SuggestionType::NewFeature` also yielding `"NewFeature"`. +/// +/// Explicitly matching on the `Unknown` variant should +/// be avoided for two reasons: +/// - The inner data `UnknownVariantValue` is opaque, and no further information can be extracted. +/// - It might inadvertently shadow other intended match arms. +#[allow(missing_docs)] // documentation missing in model +#[non_exhaustive] +#[derive( + ::std::clone::Clone, + ::std::cmp::Eq, + ::std::cmp::Ord, + ::std::cmp::PartialEq, + ::std::cmp::PartialOrd, + ::std::fmt::Debug, + ::std::hash::Hash, +)] +pub enum SuggestionType { + #[allow(missing_docs)] // documentation missing in model + Completions, + #[allow(missing_docs)] // documentation missing in model + Edits, + /// `Unknown` contains new variants that have been added since this code was generated. + #[deprecated( + note = "Don't directly match on `Unknown`. See the docs on this enum for the correct way to handle unknown variants." + )] + Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue), +} +impl ::std::convert::From<&str> for SuggestionType { + fn from(s: &str) -> Self { + match s { + "COMPLETIONS" => SuggestionType::Completions, + "EDITS" => SuggestionType::Edits, + other => SuggestionType::Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue( + other.to_owned(), + )), + } + } +} +impl ::std::str::FromStr for SuggestionType { + type Err = ::std::convert::Infallible; + + fn from_str(s: &str) -> ::std::result::Result::Err> { + ::std::result::Result::Ok(SuggestionType::from(s)) + } +} +impl SuggestionType { + /// Returns the `&str` value of the enum member. + pub fn as_str(&self) -> &str { + match self { + SuggestionType::Completions => "COMPLETIONS", + SuggestionType::Edits => "EDITS", + SuggestionType::Unknown(value) => value.as_str(), + } + } + + /// Returns all the `&str` representations of the enum members. + pub const fn values() -> &'static [&'static str] { + &["COMPLETIONS", "EDITS"] + } +} +impl ::std::convert::AsRef for SuggestionType { + fn as_ref(&self) -> &str { + self.as_str() + } +} +impl SuggestionType { + /// Parses the enum value while disallowing unknown variants. + /// + /// Unknown variants will result in an error. + pub fn try_parse(value: &str) -> ::std::result::Result { + match Self::from(value) { + #[allow(deprecated)] + Self::Unknown(_) => ::std::result::Result::Err(crate::error::UnknownVariantError::new(value)), + known => Ok(known), + } + } +} +impl ::std::fmt::Display for SuggestionType { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + match self { + SuggestionType::Completions => write!(f, "COMPLETIONS"), + SuggestionType::Edits => write!(f, "EDITS"), + SuggestionType::Unknown(value) => write!(f, "{}", value), + } + } +} diff --git a/crates/amzn-codewhisperer-client/src/types/_upgrade_capability.rs b/crates/amzn-codewhisperer-client/src/types/_upgrade_capability.rs new file mode 100644 index 0000000000..b1d1dfc314 --- /dev/null +++ b/crates/amzn-codewhisperer-client/src/types/_upgrade_capability.rs @@ -0,0 +1,118 @@ +// Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT. + +/// When writing a match expression against `UpgradeCapability`, it is important to ensure +/// your code is forward-compatible. That is, if a match arm handles a case for a +/// feature that is supported by the service but has not been represented as an enum +/// variant in a current version of SDK, your code should continue to work when you +/// upgrade SDK to a future version in which the enum does include a variant for that +/// feature. +/// +/// Here is an example of how you can make a match expression forward-compatible: +/// +/// ```text +/// # let upgradecapability = unimplemented!(); +/// match upgradecapability { +/// UpgradeCapability::UpgradeCapable => { /* ... */ }, +/// UpgradeCapability::UpgradeIncapable => { /* ... */ }, +/// other @ _ if other.as_str() == "NewFeature" => { /* handles a case for `NewFeature` */ }, +/// _ => { /* ... */ }, +/// } +/// ``` +/// The above code demonstrates that when `upgradecapability` represents +/// `NewFeature`, the execution path will lead to the second last match arm, +/// even though the enum does not contain a variant `UpgradeCapability::NewFeature` +/// in the current version of SDK. The reason is that the variable `other`, +/// created by the `@` operator, is bound to +/// `UpgradeCapability::Unknown(UnknownVariantValue("NewFeature".to_owned()))` +/// and calling `as_str` on it yields `"NewFeature"`. +/// This match expression is forward-compatible when executed with a newer +/// version of SDK where the variant `UpgradeCapability::NewFeature` is defined. +/// Specifically, when `upgradecapability` represents `NewFeature`, +/// the execution path will hit the second last match arm as before by virtue of +/// calling `as_str` on `UpgradeCapability::NewFeature` also yielding `"NewFeature"`. +/// +/// Explicitly matching on the `Unknown` variant should +/// be avoided for two reasons: +/// - The inner data `UnknownVariantValue` is opaque, and no further information can be extracted. +/// - It might inadvertently shadow other intended match arms. +#[allow(missing_docs)] // documentation missing in model +#[non_exhaustive] +#[derive( + ::std::clone::Clone, + ::std::cmp::Eq, + ::std::cmp::Ord, + ::std::cmp::PartialEq, + ::std::cmp::PartialOrd, + ::std::fmt::Debug, + ::std::hash::Hash, +)] +pub enum UpgradeCapability { + /// this user is able to upgrade + UpgradeCapable, + /// this user is unable to upgrade + UpgradeIncapable, + /// `Unknown` contains new variants that have been added since this code was generated. + #[deprecated( + note = "Don't directly match on `Unknown`. See the docs on this enum for the correct way to handle unknown variants." + )] + Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue), +} +impl ::std::convert::From<&str> for UpgradeCapability { + fn from(s: &str) -> Self { + match s { + "UPGRADE_CAPABLE" => UpgradeCapability::UpgradeCapable, + "UPGRADE_INCAPABLE" => UpgradeCapability::UpgradeIncapable, + other => UpgradeCapability::Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue( + other.to_owned(), + )), + } + } +} +impl ::std::str::FromStr for UpgradeCapability { + type Err = ::std::convert::Infallible; + + fn from_str(s: &str) -> ::std::result::Result::Err> { + ::std::result::Result::Ok(UpgradeCapability::from(s)) + } +} +impl UpgradeCapability { + /// Returns the `&str` value of the enum member. + pub fn as_str(&self) -> &str { + match self { + UpgradeCapability::UpgradeCapable => "UPGRADE_CAPABLE", + UpgradeCapability::UpgradeIncapable => "UPGRADE_INCAPABLE", + UpgradeCapability::Unknown(value) => value.as_str(), + } + } + + /// Returns all the `&str` representations of the enum members. + pub const fn values() -> &'static [&'static str] { + &["UPGRADE_CAPABLE", "UPGRADE_INCAPABLE"] + } +} +impl ::std::convert::AsRef for UpgradeCapability { + fn as_ref(&self) -> &str { + self.as_str() + } +} +impl UpgradeCapability { + /// Parses the enum value while disallowing unknown variants. + /// + /// Unknown variants will result in an error. + pub fn try_parse(value: &str) -> ::std::result::Result { + match Self::from(value) { + #[allow(deprecated)] + Self::Unknown(_) => ::std::result::Result::Err(crate::error::UnknownVariantError::new(value)), + known => Ok(known), + } + } +} +impl ::std::fmt::Display for UpgradeCapability { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + match self { + UpgradeCapability::UpgradeCapable => write!(f, "UPGRADE_CAPABLE"), + UpgradeCapability::UpgradeIncapable => write!(f, "UPGRADE_INCAPABLE"), + UpgradeCapability::Unknown(value) => write!(f, "{}", value), + } + } +} diff --git a/crates/amzn-codewhisperer-client/src/types/_user_context.rs b/crates/amzn-codewhisperer-client/src/types/_user_context.rs index ff0fe2cb98..aba4fd0492 100644 --- a/crates/amzn-codewhisperer-client/src/types/_user_context.rs +++ b/crates/amzn-codewhisperer-client/src/types/_user_context.rs @@ -13,6 +13,10 @@ pub struct UserContext { pub client_id: ::std::option::Option<::std::string::String>, #[allow(missing_docs)] // documentation missing in model pub ide_version: ::std::option::Option<::std::string::String>, + #[allow(missing_docs)] // documentation missing in model + pub plugin_version: ::std::option::Option<::std::string::String>, + #[allow(missing_docs)] // documentation missing in model + pub lsp_version: ::std::option::Option<::std::string::String>, } impl UserContext { #[allow(missing_docs)] // documentation missing in model @@ -40,6 +44,16 @@ impl UserContext { pub fn ide_version(&self) -> ::std::option::Option<&str> { self.ide_version.as_deref() } + + #[allow(missing_docs)] // documentation missing in model + pub fn plugin_version(&self) -> ::std::option::Option<&str> { + self.plugin_version.as_deref() + } + + #[allow(missing_docs)] // documentation missing in model + pub fn lsp_version(&self) -> ::std::option::Option<&str> { + self.lsp_version.as_deref() + } } impl UserContext { /// Creates a new builder-style object to manufacture @@ -58,6 +72,8 @@ pub struct UserContextBuilder { pub(crate) product: ::std::option::Option<::std::string::String>, pub(crate) client_id: ::std::option::Option<::std::string::String>, pub(crate) ide_version: ::std::option::Option<::std::string::String>, + pub(crate) plugin_version: ::std::option::Option<::std::string::String>, + pub(crate) lsp_version: ::std::option::Option<::std::string::String>, } impl UserContextBuilder { #[allow(missing_docs)] // documentation missing in model @@ -148,6 +164,40 @@ impl UserContextBuilder { &self.ide_version } + #[allow(missing_docs)] // documentation missing in model + pub fn plugin_version(mut self, input: impl ::std::convert::Into<::std::string::String>) -> Self { + self.plugin_version = ::std::option::Option::Some(input.into()); + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn set_plugin_version(mut self, input: ::std::option::Option<::std::string::String>) -> Self { + self.plugin_version = input; + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn get_plugin_version(&self) -> &::std::option::Option<::std::string::String> { + &self.plugin_version + } + + #[allow(missing_docs)] // documentation missing in model + pub fn lsp_version(mut self, input: impl ::std::convert::Into<::std::string::String>) -> Self { + self.lsp_version = ::std::option::Option::Some(input.into()); + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn set_lsp_version(mut self, input: ::std::option::Option<::std::string::String>) -> Self { + self.lsp_version = input; + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn get_lsp_version(&self) -> &::std::option::Option<::std::string::String> { + &self.lsp_version + } + /// Consumes the builder and constructs a [`UserContext`](crate::types::UserContext). /// This method will fail if any of the following fields are not set: /// - [`ide_category`](crate::types::builders::UserContextBuilder::ide_category) @@ -177,6 +227,8 @@ impl UserContextBuilder { })?, client_id: self.client_id, ide_version: self.ide_version, + plugin_version: self.plugin_version, + lsp_version: self.lsp_version, }) } } diff --git a/crates/amzn-codewhisperer-client/src/types/_user_trigger_decision_event.rs b/crates/amzn-codewhisperer-client/src/types/_user_trigger_decision_event.rs index 4187e38a46..54ad9d446d 100644 --- a/crates/amzn-codewhisperer-client/src/types/_user_trigger_decision_event.rs +++ b/crates/amzn-codewhisperer-client/src/types/_user_trigger_decision_event.rs @@ -41,6 +41,8 @@ pub struct UserTriggerDecisionEvent { pub deleted_character_count: i32, #[allow(missing_docs)] // documentation missing in model pub streak_length: i32, + #[allow(missing_docs)] // documentation missing in model + pub suggestion_type: ::std::option::Option, } impl UserTriggerDecisionEvent { #[allow(missing_docs)] // documentation missing in model @@ -145,6 +147,11 @@ impl UserTriggerDecisionEvent { pub fn streak_length(&self) -> i32 { self.streak_length } + + #[allow(missing_docs)] // documentation missing in model + pub fn suggestion_type(&self) -> ::std::option::Option<&crate::types::SuggestionType> { + self.suggestion_type.as_ref() + } } impl UserTriggerDecisionEvent { /// Creates a new builder-style object to manufacture @@ -177,6 +184,7 @@ pub struct UserTriggerDecisionEventBuilder { pub(crate) added_character_count: ::std::option::Option, pub(crate) deleted_character_count: ::std::option::Option, pub(crate) streak_length: ::std::option::Option, + pub(crate) suggestion_type: ::std::option::Option, } impl UserTriggerDecisionEventBuilder { #[allow(missing_docs)] // documentation missing in model @@ -529,6 +537,23 @@ impl UserTriggerDecisionEventBuilder { &self.streak_length } + #[allow(missing_docs)] // documentation missing in model + pub fn suggestion_type(mut self, input: crate::types::SuggestionType) -> Self { + self.suggestion_type = ::std::option::Option::Some(input); + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn set_suggestion_type(mut self, input: ::std::option::Option) -> Self { + self.suggestion_type = input; + self + } + + #[allow(missing_docs)] // documentation missing in model + pub fn get_suggestion_type(&self) -> &::std::option::Option { + &self.suggestion_type + } + /// Consumes the builder and constructs a /// [`UserTriggerDecisionEvent`](crate::types::UserTriggerDecisionEvent). This method will /// fail if any of the following fields are not set: @@ -598,6 +623,7 @@ impl UserTriggerDecisionEventBuilder { added_character_count: self.added_character_count.unwrap_or_default(), deleted_character_count: self.deleted_character_count.unwrap_or_default(), streak_length: self.streak_length.unwrap_or_default(), + suggestion_type: self.suggestion_type, }) } } diff --git a/crates/amzn-codewhisperer-streaming-client/Cargo.toml b/crates/amzn-codewhisperer-streaming-client/Cargo.toml index 9308d63d08..0f913fa88c 100644 --- a/crates/amzn-codewhisperer-streaming-client/Cargo.toml +++ b/crates/amzn-codewhisperer-streaming-client/Cargo.toml @@ -12,7 +12,7 @@ [package] edition = "2021" name = "amzn-codewhisperer-streaming-client" -version = "0.1.10231" +version = "0.1.10613" authors = ["Grant Gurvis "] build = false exclude = [ diff --git a/crates/amzn-codewhisperer-streaming-client/src/types/_access_denied_exception_reason.rs b/crates/amzn-codewhisperer-streaming-client/src/types/_access_denied_exception_reason.rs index 3c0f71561f..a4d9001996 100644 --- a/crates/amzn-codewhisperer-streaming-client/src/types/_access_denied_exception_reason.rs +++ b/crates/amzn-codewhisperer-streaming-client/src/types/_access_denied_exception_reason.rs @@ -12,6 +12,7 @@ /// ```text /// # let accessdeniedexceptionreason = unimplemented!(); /// match accessdeniedexceptionreason { +/// AccessDeniedExceptionReason::FeatureNotSupported => { /* ... */ }, /// AccessDeniedExceptionReason::TemporarilySuspended => { /* ... */ }, /// AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess => { /* ... */ }, /// AccessDeniedExceptionReason::UnauthorizedWorkspaceContextFeatureAccess => { /* ... */ }, @@ -49,6 +50,8 @@ ::std::hash::Hash, )] pub enum AccessDeniedExceptionReason { + #[allow(missing_docs)] // documentation missing in model + FeatureNotSupported, #[allow(missing_docs)] // documentation missing in model TemporarilySuspended, #[allow(missing_docs)] // documentation missing in model @@ -64,6 +67,7 @@ pub enum AccessDeniedExceptionReason { impl ::std::convert::From<&str> for AccessDeniedExceptionReason { fn from(s: &str) -> Self { match s { + "FEATURE_NOT_SUPPORTED" => AccessDeniedExceptionReason::FeatureNotSupported, "TEMPORARILY_SUSPENDED" => AccessDeniedExceptionReason::TemporarilySuspended, "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS" => { AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess @@ -88,6 +92,7 @@ impl AccessDeniedExceptionReason { /// Returns the `&str` value of the enum member. pub fn as_str(&self) -> &str { match self { + AccessDeniedExceptionReason::FeatureNotSupported => "FEATURE_NOT_SUPPORTED", AccessDeniedExceptionReason::TemporarilySuspended => "TEMPORARILY_SUSPENDED", AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess => { "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS" @@ -102,6 +107,7 @@ impl AccessDeniedExceptionReason { /// Returns all the `&str` representations of the enum members. pub const fn values() -> &'static [&'static str] { &[ + "FEATURE_NOT_SUPPORTED", "TEMPORARILY_SUSPENDED", "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS", "UNAUTHORIZED_WORKSPACE_CONTEXT_FEATURE_ACCESS", @@ -128,6 +134,7 @@ impl AccessDeniedExceptionReason { impl ::std::fmt::Display for AccessDeniedExceptionReason { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { match self { + AccessDeniedExceptionReason::FeatureNotSupported => write!(f, "FEATURE_NOT_SUPPORTED"), AccessDeniedExceptionReason::TemporarilySuspended => write!(f, "TEMPORARILY_SUSPENDED"), AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess => { write!(f, "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS") diff --git a/crates/amzn-codewhisperer-streaming-client/src/types/_origin.rs b/crates/amzn-codewhisperer-streaming-client/src/types/_origin.rs index 4984c919de..be6c84095a 100644 --- a/crates/amzn-codewhisperer-streaming-client/src/types/_origin.rs +++ b/crates/amzn-codewhisperer-streaming-client/src/types/_origin.rs @@ -19,6 +19,7 @@ /// Origin::Documentation => { /* ... */ }, /// Origin::Gitlab => { /* ... */ }, /// Origin::Ide => { /* ... */ }, +/// Origin::InlineChat => { /* ... */ }, /// Origin::Marketing => { /* ... */ }, /// Origin::Md => { /* ... */ }, /// Origin::MdCe => { /* ... */ }, @@ -28,6 +29,7 @@ /// Origin::QDevBext => { /* ... */ }, /// Origin::SageMaker => { /* ... */ }, /// Origin::ServiceInternal => { /* ... */ }, +/// Origin::SmAiStudioIde => { /* ... */ }, /// Origin::UnifiedSearch => { /* ... */ }, /// Origin::UnknownValue => { /* ... */ }, /// other @ _ if other.as_str() == "NewFeature" => { /* handles a case for `NewFeature` */ }, @@ -80,6 +82,8 @@ pub enum Origin { Gitlab, /// Any IDE caller. Ide, + /// Q Developer Inline Chat. + InlineChat, /// AWS Marketing Website (https://aws.amazon.com) Marketing, /// MD. @@ -99,6 +103,8 @@ pub enum Origin { /// Internal Service Traffic (Integ Tests, Canaries, etc.). This is the default when no Origin /// header present in request. ServiceInternal, + /// SageMaker AI Studio IDE Chat + SmAiStudioIde, /// Unified Search in AWS Management Console (https://.console.aws.amazon.com) UnifiedSearch, /// Origin header is not set. @@ -121,6 +127,7 @@ impl ::std::convert::From<&str> for Origin { "DOCUMENTATION" => Origin::Documentation, "GITLAB" => Origin::Gitlab, "IDE" => Origin::Ide, + "INLINE_CHAT" => Origin::InlineChat, "MARKETING" => Origin::Marketing, "MD" => Origin::Md, "MD_CE" => Origin::MdCe, @@ -130,6 +137,7 @@ impl ::std::convert::From<&str> for Origin { "Q_DEV_BEXT" => Origin::QDevBext, "SAGE_MAKER" => Origin::SageMaker, "SERVICE_INTERNAL" => Origin::ServiceInternal, + "SM_AI_STUDIO_IDE" => Origin::SmAiStudioIde, "UNIFIED_SEARCH" => Origin::UnifiedSearch, "UNKNOWN" => Origin::UnknownValue, other => Origin::Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue( @@ -156,6 +164,7 @@ impl Origin { Origin::Documentation => "DOCUMENTATION", Origin::Gitlab => "GITLAB", Origin::Ide => "IDE", + Origin::InlineChat => "INLINE_CHAT", Origin::Marketing => "MARKETING", Origin::Md => "MD", Origin::MdCe => "MD_CE", @@ -165,6 +174,7 @@ impl Origin { Origin::QDevBext => "Q_DEV_BEXT", Origin::SageMaker => "SAGE_MAKER", Origin::ServiceInternal => "SERVICE_INTERNAL", + Origin::SmAiStudioIde => "SM_AI_STUDIO_IDE", Origin::UnifiedSearch => "UNIFIED_SEARCH", Origin::UnknownValue => "UNKNOWN", Origin::Unknown(value) => value.as_str(), @@ -181,6 +191,7 @@ impl Origin { "DOCUMENTATION", "GITLAB", "IDE", + "INLINE_CHAT", "MARKETING", "MD", "MD_CE", @@ -190,6 +201,7 @@ impl Origin { "Q_DEV_BEXT", "SAGE_MAKER", "SERVICE_INTERNAL", + "SM_AI_STUDIO_IDE", "UNIFIED_SEARCH", "UNKNOWN", ] @@ -222,6 +234,7 @@ impl ::std::fmt::Display for Origin { Origin::Documentation => write!(f, "DOCUMENTATION"), Origin::Gitlab => write!(f, "GITLAB"), Origin::Ide => write!(f, "IDE"), + Origin::InlineChat => write!(f, "INLINE_CHAT"), Origin::Marketing => write!(f, "MARKETING"), Origin::Md => write!(f, "MD"), Origin::MdCe => write!(f, "MD_CE"), @@ -231,6 +244,7 @@ impl ::std::fmt::Display for Origin { Origin::QDevBext => write!(f, "Q_DEV_BEXT"), Origin::SageMaker => write!(f, "SAGE_MAKER"), Origin::ServiceInternal => write!(f, "SERVICE_INTERNAL"), + Origin::SmAiStudioIde => write!(f, "SM_AI_STUDIO_IDE"), Origin::UnifiedSearch => write!(f, "UNIFIED_SEARCH"), Origin::UnknownValue => write!(f, "UNKNOWN"), Origin::Unknown(value) => write!(f, "{}", value), diff --git a/crates/amzn-consolas-client/Cargo.toml b/crates/amzn-consolas-client/Cargo.toml index ab59eb78ca..a6e65cffe0 100644 --- a/crates/amzn-consolas-client/Cargo.toml +++ b/crates/amzn-consolas-client/Cargo.toml @@ -12,7 +12,7 @@ [package] edition = "2021" name = "amzn-consolas-client" -version = "0.1.10231" +version = "0.1.10613" authors = ["Grant Gurvis "] build = false exclude = [ diff --git a/crates/amzn-consolas-client/src/types/_access_denied_exception_reason.rs b/crates/amzn-consolas-client/src/types/_access_denied_exception_reason.rs index 3c0f71561f..a4d9001996 100644 --- a/crates/amzn-consolas-client/src/types/_access_denied_exception_reason.rs +++ b/crates/amzn-consolas-client/src/types/_access_denied_exception_reason.rs @@ -12,6 +12,7 @@ /// ```text /// # let accessdeniedexceptionreason = unimplemented!(); /// match accessdeniedexceptionreason { +/// AccessDeniedExceptionReason::FeatureNotSupported => { /* ... */ }, /// AccessDeniedExceptionReason::TemporarilySuspended => { /* ... */ }, /// AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess => { /* ... */ }, /// AccessDeniedExceptionReason::UnauthorizedWorkspaceContextFeatureAccess => { /* ... */ }, @@ -49,6 +50,8 @@ ::std::hash::Hash, )] pub enum AccessDeniedExceptionReason { + #[allow(missing_docs)] // documentation missing in model + FeatureNotSupported, #[allow(missing_docs)] // documentation missing in model TemporarilySuspended, #[allow(missing_docs)] // documentation missing in model @@ -64,6 +67,7 @@ pub enum AccessDeniedExceptionReason { impl ::std::convert::From<&str> for AccessDeniedExceptionReason { fn from(s: &str) -> Self { match s { + "FEATURE_NOT_SUPPORTED" => AccessDeniedExceptionReason::FeatureNotSupported, "TEMPORARILY_SUSPENDED" => AccessDeniedExceptionReason::TemporarilySuspended, "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS" => { AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess @@ -88,6 +92,7 @@ impl AccessDeniedExceptionReason { /// Returns the `&str` value of the enum member. pub fn as_str(&self) -> &str { match self { + AccessDeniedExceptionReason::FeatureNotSupported => "FEATURE_NOT_SUPPORTED", AccessDeniedExceptionReason::TemporarilySuspended => "TEMPORARILY_SUSPENDED", AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess => { "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS" @@ -102,6 +107,7 @@ impl AccessDeniedExceptionReason { /// Returns all the `&str` representations of the enum members. pub const fn values() -> &'static [&'static str] { &[ + "FEATURE_NOT_SUPPORTED", "TEMPORARILY_SUSPENDED", "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS", "UNAUTHORIZED_WORKSPACE_CONTEXT_FEATURE_ACCESS", @@ -128,6 +134,7 @@ impl AccessDeniedExceptionReason { impl ::std::fmt::Display for AccessDeniedExceptionReason { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { match self { + AccessDeniedExceptionReason::FeatureNotSupported => write!(f, "FEATURE_NOT_SUPPORTED"), AccessDeniedExceptionReason::TemporarilySuspended => write!(f, "TEMPORARILY_SUSPENDED"), AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess => { write!(f, "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS") diff --git a/crates/amzn-qdeveloper-streaming-client/Cargo.toml b/crates/amzn-qdeveloper-streaming-client/Cargo.toml index b07810cc5b..f05e61cbca 100644 --- a/crates/amzn-qdeveloper-streaming-client/Cargo.toml +++ b/crates/amzn-qdeveloper-streaming-client/Cargo.toml @@ -12,7 +12,7 @@ [package] edition = "2021" name = "amzn-qdeveloper-streaming-client" -version = "0.1.10231" +version = "0.1.10613" authors = ["Grant Gurvis "] build = false exclude = [ diff --git a/crates/amzn-qdeveloper-streaming-client/src/types/_access_denied_exception_reason.rs b/crates/amzn-qdeveloper-streaming-client/src/types/_access_denied_exception_reason.rs index 3c0f71561f..a4d9001996 100644 --- a/crates/amzn-qdeveloper-streaming-client/src/types/_access_denied_exception_reason.rs +++ b/crates/amzn-qdeveloper-streaming-client/src/types/_access_denied_exception_reason.rs @@ -12,6 +12,7 @@ /// ```text /// # let accessdeniedexceptionreason = unimplemented!(); /// match accessdeniedexceptionreason { +/// AccessDeniedExceptionReason::FeatureNotSupported => { /* ... */ }, /// AccessDeniedExceptionReason::TemporarilySuspended => { /* ... */ }, /// AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess => { /* ... */ }, /// AccessDeniedExceptionReason::UnauthorizedWorkspaceContextFeatureAccess => { /* ... */ }, @@ -49,6 +50,8 @@ ::std::hash::Hash, )] pub enum AccessDeniedExceptionReason { + #[allow(missing_docs)] // documentation missing in model + FeatureNotSupported, #[allow(missing_docs)] // documentation missing in model TemporarilySuspended, #[allow(missing_docs)] // documentation missing in model @@ -64,6 +67,7 @@ pub enum AccessDeniedExceptionReason { impl ::std::convert::From<&str> for AccessDeniedExceptionReason { fn from(s: &str) -> Self { match s { + "FEATURE_NOT_SUPPORTED" => AccessDeniedExceptionReason::FeatureNotSupported, "TEMPORARILY_SUSPENDED" => AccessDeniedExceptionReason::TemporarilySuspended, "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS" => { AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess @@ -88,6 +92,7 @@ impl AccessDeniedExceptionReason { /// Returns the `&str` value of the enum member. pub fn as_str(&self) -> &str { match self { + AccessDeniedExceptionReason::FeatureNotSupported => "FEATURE_NOT_SUPPORTED", AccessDeniedExceptionReason::TemporarilySuspended => "TEMPORARILY_SUSPENDED", AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess => { "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS" @@ -102,6 +107,7 @@ impl AccessDeniedExceptionReason { /// Returns all the `&str` representations of the enum members. pub const fn values() -> &'static [&'static str] { &[ + "FEATURE_NOT_SUPPORTED", "TEMPORARILY_SUSPENDED", "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS", "UNAUTHORIZED_WORKSPACE_CONTEXT_FEATURE_ACCESS", @@ -128,6 +134,7 @@ impl AccessDeniedExceptionReason { impl ::std::fmt::Display for AccessDeniedExceptionReason { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { match self { + AccessDeniedExceptionReason::FeatureNotSupported => write!(f, "FEATURE_NOT_SUPPORTED"), AccessDeniedExceptionReason::TemporarilySuspended => write!(f, "TEMPORARILY_SUSPENDED"), AccessDeniedExceptionReason::UnauthorizedCustomizationResourceAccess => { write!(f, "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS") diff --git a/crates/amzn-qdeveloper-streaming-client/src/types/_origin.rs b/crates/amzn-qdeveloper-streaming-client/src/types/_origin.rs index 4984c919de..be6c84095a 100644 --- a/crates/amzn-qdeveloper-streaming-client/src/types/_origin.rs +++ b/crates/amzn-qdeveloper-streaming-client/src/types/_origin.rs @@ -19,6 +19,7 @@ /// Origin::Documentation => { /* ... */ }, /// Origin::Gitlab => { /* ... */ }, /// Origin::Ide => { /* ... */ }, +/// Origin::InlineChat => { /* ... */ }, /// Origin::Marketing => { /* ... */ }, /// Origin::Md => { /* ... */ }, /// Origin::MdCe => { /* ... */ }, @@ -28,6 +29,7 @@ /// Origin::QDevBext => { /* ... */ }, /// Origin::SageMaker => { /* ... */ }, /// Origin::ServiceInternal => { /* ... */ }, +/// Origin::SmAiStudioIde => { /* ... */ }, /// Origin::UnifiedSearch => { /* ... */ }, /// Origin::UnknownValue => { /* ... */ }, /// other @ _ if other.as_str() == "NewFeature" => { /* handles a case for `NewFeature` */ }, @@ -80,6 +82,8 @@ pub enum Origin { Gitlab, /// Any IDE caller. Ide, + /// Q Developer Inline Chat. + InlineChat, /// AWS Marketing Website (https://aws.amazon.com) Marketing, /// MD. @@ -99,6 +103,8 @@ pub enum Origin { /// Internal Service Traffic (Integ Tests, Canaries, etc.). This is the default when no Origin /// header present in request. ServiceInternal, + /// SageMaker AI Studio IDE Chat + SmAiStudioIde, /// Unified Search in AWS Management Console (https://.console.aws.amazon.com) UnifiedSearch, /// Origin header is not set. @@ -121,6 +127,7 @@ impl ::std::convert::From<&str> for Origin { "DOCUMENTATION" => Origin::Documentation, "GITLAB" => Origin::Gitlab, "IDE" => Origin::Ide, + "INLINE_CHAT" => Origin::InlineChat, "MARKETING" => Origin::Marketing, "MD" => Origin::Md, "MD_CE" => Origin::MdCe, @@ -130,6 +137,7 @@ impl ::std::convert::From<&str> for Origin { "Q_DEV_BEXT" => Origin::QDevBext, "SAGE_MAKER" => Origin::SageMaker, "SERVICE_INTERNAL" => Origin::ServiceInternal, + "SM_AI_STUDIO_IDE" => Origin::SmAiStudioIde, "UNIFIED_SEARCH" => Origin::UnifiedSearch, "UNKNOWN" => Origin::UnknownValue, other => Origin::Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue( @@ -156,6 +164,7 @@ impl Origin { Origin::Documentation => "DOCUMENTATION", Origin::Gitlab => "GITLAB", Origin::Ide => "IDE", + Origin::InlineChat => "INLINE_CHAT", Origin::Marketing => "MARKETING", Origin::Md => "MD", Origin::MdCe => "MD_CE", @@ -165,6 +174,7 @@ impl Origin { Origin::QDevBext => "Q_DEV_BEXT", Origin::SageMaker => "SAGE_MAKER", Origin::ServiceInternal => "SERVICE_INTERNAL", + Origin::SmAiStudioIde => "SM_AI_STUDIO_IDE", Origin::UnifiedSearch => "UNIFIED_SEARCH", Origin::UnknownValue => "UNKNOWN", Origin::Unknown(value) => value.as_str(), @@ -181,6 +191,7 @@ impl Origin { "DOCUMENTATION", "GITLAB", "IDE", + "INLINE_CHAT", "MARKETING", "MD", "MD_CE", @@ -190,6 +201,7 @@ impl Origin { "Q_DEV_BEXT", "SAGE_MAKER", "SERVICE_INTERNAL", + "SM_AI_STUDIO_IDE", "UNIFIED_SEARCH", "UNKNOWN", ] @@ -222,6 +234,7 @@ impl ::std::fmt::Display for Origin { Origin::Documentation => write!(f, "DOCUMENTATION"), Origin::Gitlab => write!(f, "GITLAB"), Origin::Ide => write!(f, "IDE"), + Origin::InlineChat => write!(f, "INLINE_CHAT"), Origin::Marketing => write!(f, "MARKETING"), Origin::Md => write!(f, "MD"), Origin::MdCe => write!(f, "MD_CE"), @@ -231,6 +244,7 @@ impl ::std::fmt::Display for Origin { Origin::QDevBext => write!(f, "Q_DEV_BEXT"), Origin::SageMaker => write!(f, "SAGE_MAKER"), Origin::ServiceInternal => write!(f, "SERVICE_INTERNAL"), + Origin::SmAiStudioIde => write!(f, "SM_AI_STUDIO_IDE"), Origin::UnifiedSearch => write!(f, "UNIFIED_SEARCH"), Origin::UnknownValue => write!(f, "UNKNOWN"), Origin::Unknown(value) => write!(f, "{}", value), diff --git a/crates/chat-cli/src/cli/chat/cli/knowledge.rs b/crates/chat-cli/src/cli/chat/cli/knowledge.rs index d2b9e724d1..cf0ea26a55 100644 --- a/crates/chat-cli/src/cli/chat/cli/knowledge.rs +++ b/crates/chat-cli/src/cli/chat/cli/knowledge.rs @@ -605,7 +605,7 @@ mod tests { #[test] fn test_include_exclude_patterns_parsing() { // Test that include and exclude patterns are parsed correctly - let result = TestCli::try_parse_from(&[ + let result = TestCli::try_parse_from([ "test", "add", "/some/path", @@ -636,7 +636,7 @@ mod tests { #[test] fn test_clap_markdown_parsing_issue() { - let help_result = TestCli::try_parse_from(&["test", "add", "--help"]); + let help_result = TestCli::try_parse_from(["test", "add", "--help"]); match help_result { Err(err) if err.kind() == clap::error::ErrorKind::DisplayHelp => { // This is expected for --help @@ -650,7 +650,7 @@ mod tests { #[test] fn test_empty_patterns_allowed() { // Test that commands work without any patterns - let result = TestCli::try_parse_from(&["test", "add", "/some/path"]); + let result = TestCli::try_parse_from(["test", "add", "/some/path"]); assert!(result.is_ok()); let cli = result.unwrap(); @@ -669,7 +669,7 @@ mod tests { #[test] fn test_multiple_include_patterns() { // Test multiple include patterns - let result = TestCli::try_parse_from(&[ + let result = TestCli::try_parse_from([ "test", "add", "/some/path", @@ -694,7 +694,7 @@ mod tests { #[test] fn test_multiple_exclude_patterns() { // Test multiple exclude patterns - let result = TestCli::try_parse_from(&[ + let result = TestCli::try_parse_from([ "test", "add", "/some/path", diff --git a/crates/chat-cli/src/cli/chat/tools/introspect.rs b/crates/chat-cli/src/cli/chat/tools/introspect.rs index a78184e2b8..975e309a76 100644 --- a/crates/chat-cli/src/cli/chat/tools/introspect.rs +++ b/crates/chat-cli/src/cli/chat/tools/introspect.rs @@ -134,6 +134,7 @@ impl Introspect { queue, style, }; + _ = self; queue!(output, style::Print("Introspecting to get you the right information"))?; Ok(()) } From c3276a42bd68271b04b14c93271095986ea6d631 Mon Sep 17 00:00:00 2001 From: abhraina-aws Date: Wed, 27 Aug 2025 17:21:13 -0700 Subject: [PATCH 8/8] feat: add tangent mode duration tracking telemetry (#2710) - Track time spent in tangent mode sessions - Send duration metric when exiting tangent mode via /tangent command - Use codewhispererterminal_chatSlashCommandExecuted metric with: - command: 'tangent' - subcommand: 'exit' - value: duration in seconds - Add comprehensive tests for duration calculation - Enables analytics on tangent mode usage patterns --- crates/chat-cli/src/cli/chat/cli/tangent.rs | 58 ++++++++++++++++++++ crates/chat-cli/src/cli/chat/conversation.rs | 48 ++++++++++++++++ crates/chat-cli/src/telemetry/core.rs | 31 +++++++++++ crates/chat-cli/src/telemetry/mod.rs | 17 ++++++ 4 files changed, 154 insertions(+) diff --git a/crates/chat-cli/src/cli/chat/cli/tangent.rs b/crates/chat-cli/src/cli/chat/cli/tangent.rs index f21ddf10b9..334156942b 100644 --- a/crates/chat-cli/src/cli/chat/cli/tangent.rs +++ b/crates/chat-cli/src/cli/chat/cli/tangent.rs @@ -18,7 +18,25 @@ pub struct TangentArgs; impl TangentArgs { pub async fn execute(self, os: &Os, session: &mut ChatSession) -> Result { if session.conversation.is_in_tangent_mode() { + // Get duration before exiting tangent mode + let duration_seconds = session.conversation.get_tangent_duration_seconds().unwrap_or(0); + session.conversation.exit_tangent_mode(); + + // Send telemetry for tangent mode session + if let Err(err) = os + .telemetry + .send_tangent_mode_session( + &os.database, + session.conversation.conversation_id().to_string(), + crate::telemetry::TelemetryResult::Succeeded, + crate::telemetry::core::TangentModeSessionArgs { duration_seconds }, + ) + .await + { + tracing::warn!(?err, "Failed to send tangent mode session telemetry"); + } + execute!( session.stderr, style::SetForegroundColor(Color::DarkGrey), @@ -69,3 +87,43 @@ impl TangentArgs { }) } } + +#[cfg(test)] +mod tests { + use crate::cli::agent::Agents; + use crate::cli::chat::conversation::ConversationState; + use crate::cli::chat::tool_manager::ToolManager; + use crate::os::Os; + + #[tokio::test] + async fn test_tangent_mode_duration_tracking() { + let mut os = Os::new().await.unwrap(); + let agents = Agents::default(); + let mut tool_manager = ToolManager::default(); + let mut conversation = ConversationState::new( + "test_conv_id", + agents, + tool_manager.load_tools(&mut os, &mut vec![]).await.unwrap(), + tool_manager, + None, + &os, + false, // mcp_enabled + ) + .await; + + // Test entering tangent mode + assert!(!conversation.is_in_tangent_mode()); + conversation.enter_tangent_mode(); + assert!(conversation.is_in_tangent_mode()); + + // Should have a duration + let duration = conversation.get_tangent_duration_seconds(); + assert!(duration.is_some()); + assert!(duration.unwrap() >= 0); + + // Test exiting tangent mode + conversation.exit_tangent_mode(); + assert!(!conversation.is_in_tangent_mode()); + assert!(conversation.get_tangent_duration_seconds().is_none()); + } +} diff --git a/crates/chat-cli/src/cli/chat/conversation.rs b/crates/chat-cli/src/cli/chat/conversation.rs index 55a1797800..5b1d79d248 100644 --- a/crates/chat-cli/src/cli/chat/conversation.rs +++ b/crates/chat-cli/src/cli/chat/conversation.rs @@ -139,6 +139,9 @@ struct ConversationCheckpoint { main_next_message: Option, /// Main conversation transcript main_transcript: VecDeque, + /// Timestamp when tangent mode was entered (milliseconds since epoch) + #[serde(default = "time::OffsetDateTime::now_utc")] + tangent_start_time: time::OffsetDateTime, } impl ConversationState { @@ -229,6 +232,7 @@ impl ConversationState { main_history: self.history.clone(), main_next_message: self.next_message.clone(), main_transcript: self.transcript.clone(), + tangent_start_time: time::OffsetDateTime::now_utc(), } } @@ -247,6 +251,14 @@ impl ConversationState { } } + /// Get tangent mode duration in seconds if currently in tangent mode + pub fn get_tangent_duration_seconds(&self) -> Option { + self.tangent_state.as_ref().map(|checkpoint| { + let now = time::OffsetDateTime::now_utc(); + (now - checkpoint.tangent_start_time).whole_seconds() + }) + } + /// Exit tangent mode - restore from checkpoint pub fn exit_tangent_mode(&mut self) { if let Some(checkpoint) = self.tangent_state.take() { @@ -1412,4 +1424,40 @@ mod tests { conversation.exit_tangent_mode(); assert!(!conversation.is_in_tangent_mode()); } + + #[tokio::test] + async fn test_tangent_mode_duration() { + let mut os = Os::new().await.unwrap(); + let agents = Agents::default(); + let mut tool_manager = ToolManager::default(); + let mut conversation = ConversationState::new( + "fake_conv_id", + agents, + tool_manager.load_tools(&mut os, &mut vec![]).await.unwrap(), + tool_manager, + None, + &os, + false, // mcp_enabled + ) + .await; + + // Initially not in tangent mode, no duration + assert!(conversation.get_tangent_duration_seconds().is_none()); + + // Enter tangent mode + conversation.enter_tangent_mode(); + assert!(conversation.is_in_tangent_mode()); + + // Should have a duration (likely 0 seconds since it just started) + let duration = conversation.get_tangent_duration_seconds(); + assert!(duration.is_some()); + assert!(duration.unwrap() >= 0); + + // Exit tangent mode + conversation.exit_tangent_mode(); + assert!(!conversation.is_in_tangent_mode()); + + // No duration when not in tangent mode + assert!(conversation.get_tangent_duration_seconds().is_none()); + } } diff --git a/crates/chat-cli/src/telemetry/core.rs b/crates/chat-cli/src/telemetry/core.rs index a719687413..08ff2df185 100644 --- a/crates/chat-cli/src/telemetry/core.rs +++ b/crates/chat-cli/src/telemetry/core.rs @@ -286,6 +286,25 @@ impl Event { } .into_metric_datum(), ), + EventType::TangentModeSession { + conversation_id, + result, + args: TangentModeSessionArgs { duration_seconds }, + } => Some( + CodewhispererterminalChatSlashCommandExecuted { + create_time: self.created_time, + value: Some(duration_seconds as f64), + credential_start_url: self.credential_start_url.map(Into::into), + sso_region: self.sso_region.map(Into::into), + amazonq_conversation_id: Some(conversation_id.into()), + codewhispererterminal_chat_slash_command: Some("tangent".to_string().into()), + codewhispererterminal_chat_slash_subcommand: Some("exit".to_string().into()), + result: Some(result.to_string().into()), + reason: None, + codewhispererterminal_in_cloudshell: None, + } + .into_metric_datum(), + ), EventType::ToolUseSuggested { conversation_id, utterance_id, @@ -528,6 +547,13 @@ pub struct ChatAddedMessageParams { pub message_meta_tags: Vec, } +/// Optional fields for tangent mode session telemetry event. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)] +pub struct TangentModeSessionArgs { + /// Duration of tangent mode session in seconds + pub duration_seconds: i64, +} + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)] pub struct RecordUserTurnCompletionArgs { pub request_ids: Vec>, @@ -592,6 +618,11 @@ pub enum EventType { result: TelemetryResult, args: RecordUserTurnCompletionArgs, }, + TangentModeSession { + conversation_id: String, + result: TelemetryResult, + args: TangentModeSessionArgs, + }, ToolUseSuggested { conversation_id: String, utterance_id: Option, diff --git a/crates/chat-cli/src/telemetry/mod.rs b/crates/chat-cli/src/telemetry/mod.rs index 68e5c78d16..b1deb521b4 100644 --- a/crates/chat-cli/src/telemetry/mod.rs +++ b/crates/chat-cli/src/telemetry/mod.rs @@ -8,6 +8,7 @@ use core::{ AgentConfigInitArgs, ChatAddedMessageParams, RecordUserTurnCompletionArgs, + TangentModeSessionArgs, ToolUseEventBuilder, }; use std::str::FromStr; @@ -322,6 +323,22 @@ impl TelemetryThread { Ok(self.tx.send(telemetry_event)?) } + pub async fn send_tangent_mode_session( + &self, + database: &Database, + conversation_id: String, + result: TelemetryResult, + args: TangentModeSessionArgs, + ) -> Result<(), TelemetryError> { + let mut telemetry_event = Event::new(EventType::TangentModeSession { + conversation_id, + result, + args, + }); + set_event_metadata(database, &mut telemetry_event).await; + Ok(self.tx.send(telemetry_event)?) + } + pub async fn send_tool_use_suggested( &self, database: &Database,