diff --git a/app/src/ai/bedrock/client.rs b/app/src/ai/bedrock/client.rs index 9756a974..8aabda29 100644 --- a/app/src/ai/bedrock/client.rs +++ b/app/src/ai/bedrock/client.rs @@ -8,7 +8,7 @@ use aws_sdk_bedrockruntime::Client as BedrockRuntimeClient; use crate::settings::ai::BedrockAuthMethod; use super::external_config::ExternalBedrockConfig; -use super::convert::{build_converse_request, ConversationMessage, ToolDefinition}; +use super::convert::{build_converse_request, CachingConfig, ConversationMessage, ToolDefinition}; use super::diagnostic::BedrockDiagnosticLogger; use super::models::apply_cross_region_prefix; use super::response_translator::bedrock_stream_to_response_events; @@ -148,6 +148,13 @@ impl BedrockClient { model_id.to_string() }; + let external_config = ExternalBedrockConfig::load(); + let caching_config = CachingConfig::from_external_config(&external_config); + + if !caching_config.enabled { + log::info!("[bedrock] Prompt caching disabled (DISABLE_PROMPT_CACHING=1)"); + } + log::info!( "[bedrock] converse_stream: model={effective_model_id}, region={}, messages={}, tools={}", self.region, @@ -163,6 +170,7 @@ impl BedrockClient { temperature, None, None, + caching_config, ); if let Some(ref logger) = diagnostic_logger { diff --git a/app/src/ai/bedrock/convert.rs b/app/src/ai/bedrock/convert.rs index 58884f1a..9fe2e1a3 100644 --- a/app/src/ai/bedrock/convert.rs +++ b/app/src/ai/bedrock/convert.rs @@ -8,6 +8,32 @@ use aws_sdk_bedrockruntime::types::{ use aws_smithy_types::Document; use serde_json::Value as JsonValue; +use super::external_config::ExternalBedrockConfig; + +#[derive(Clone, Debug)] +pub struct CachingConfig { + pub enabled: bool, + pub extended_ttl_requested: bool, +} + +impl Default for CachingConfig { + fn default() -> Self { + Self { + enabled: true, + extended_ttl_requested: false, + } + } +} + +impl CachingConfig { + pub fn from_external_config(external: &ExternalBedrockConfig) -> Self { + Self { + enabled: !external.disable_prompt_caching, + extended_ttl_requested: external.enable_prompt_caching_1h, + } + } +} + pub struct ConvertedRequest { pub messages: Vec, pub system: Vec, @@ -73,11 +99,12 @@ pub fn build_converse_request( temperature: Option, top_p: Option, stop_sequences: Option>, + caching_config: CachingConfig, ) -> ConvertedRequest { - let bedrock_messages = convert_messages(messages); - let system = convert_system_prompt(system_prompt); + let bedrock_messages = convert_messages(messages, &caching_config); + let system = convert_system_prompt(system_prompt, &caching_config); let inference_config = build_inference_config(max_tokens, temperature, top_p, stop_sequences); - let tool_config = build_tool_config(tools); + let tool_config = build_tool_config(tools, &caching_config); ConvertedRequest { messages: bedrock_messages, @@ -112,7 +139,7 @@ fn json_to_document(value: JsonValue) -> Document { } } -fn convert_messages(messages: Vec) -> Vec { +fn convert_messages(messages: Vec, caching_config: &CachingConfig) -> Vec { let mut result = Vec::new(); for msg in messages { @@ -213,7 +240,7 @@ fn convert_messages(messages: Vec) -> Vec { // Add a cache point to the second-to-last message (the conversation prefix // that is stable between requests). This allows Bedrock to cache all prior // context and only process the latest message as new input tokens. - if messages.len() >= 2 { + if caching_config.enabled && messages.len() >= 2 { let cache_idx = messages.len() - 2; let msg = messages.remove(cache_idx); let mut content = msg.content().to_vec(); @@ -229,6 +256,11 @@ fn convert_messages(messages: Vec) -> Vec { .build() .expect("valid message with cache point"); messages.insert(cache_idx, cached_msg); + + if caching_config.extended_ttl_requested { + log::info!("[bedrock] Extended 1-hour caching requested (ENABLE_PROMPT_CACHING_1H=1)"); + // TODO: Use explicit TTL when AWS SDK supports it + } } messages @@ -265,18 +297,19 @@ fn coalesce_consecutive_roles(messages: Vec) -> Vec) -> Vec { +fn convert_system_prompt(system_prompt: Option, caching_config: &CachingConfig) -> Vec { match system_prompt { Some(prompt) if !prompt.is_empty() => { - vec![ - SystemContentBlock::Text(prompt), - SystemContentBlock::CachePoint( + let mut blocks = vec![SystemContentBlock::Text(prompt)]; + if caching_config.enabled { + blocks.push(SystemContentBlock::CachePoint( CachePointBlock::builder() .r#type(CachePointType::Default) .build() .expect("valid cache point"), - ), - ] + )); + } + blocks } _ => vec![], } @@ -303,7 +336,7 @@ fn build_inference_config( builder.build() } -fn build_tool_config(tools: Vec) -> Option { +fn build_tool_config(tools: Vec, caching_config: &CachingConfig) -> Option { if tools.is_empty() { return None; } @@ -323,12 +356,14 @@ fn build_tool_config(tools: Vec) -> Option { }) .collect(); - tool_specs.push(Tool::CachePoint( - CachePointBlock::builder() - .r#type(CachePointType::Default) - .build() - .expect("valid cache point"), - )); + if caching_config.enabled { + tool_specs.push(Tool::CachePoint( + CachePointBlock::builder() + .r#type(CachePointType::Default) + .build() + .expect("valid cache point"), + )); + } Some( ToolConfiguration::builder() diff --git a/app/src/ai/bedrock/convert_tests.rs b/app/src/ai/bedrock/convert_tests.rs index 26389a80..e5f4c0cf 100644 --- a/app/src/ai/bedrock/convert_tests.rs +++ b/app/src/ai/bedrock/convert_tests.rs @@ -10,7 +10,7 @@ fn test_text_message_converts_to_single_block() { content: MessageContent::Text("Hello".to_string()), }]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, vec![], 4096, None, None, None, CachingConfig::default()); assert_eq!(result.messages.len(), 1); assert_eq!(result.messages[0].role(), &ConversationRole::User); @@ -29,7 +29,7 @@ fn test_tool_use_produces_valid_json_input() { }, }]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, vec![], 4096, None, None, None, CachingConfig::default()); assert_eq!(result.messages.len(), 1); assert_eq!(result.messages[0].role(), &ConversationRole::Assistant); @@ -53,7 +53,7 @@ fn test_tool_result_with_matching_id() { }, }]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, vec![], 4096, None, None, None, CachingConfig::default()); assert_eq!(result.messages.len(), 1); match &result.messages[0].content()[0] { @@ -75,7 +75,7 @@ fn test_tool_result_error_status() { }, }]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, vec![], 4096, None, None, None, CachingConfig::default()); match &result.messages[0].content()[0] { ContentBlock::ToolResult(block) => { @@ -101,7 +101,7 @@ fn test_consecutive_same_role_messages_coalesced() { }, ]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, vec![], 4096, None, None, None, CachingConfig::default()); assert_eq!(result.messages.len(), 1); assert_eq!(result.messages[0].content().len(), 2); @@ -126,7 +126,7 @@ fn test_alternating_roles_not_coalesced() { }, ]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, vec![], 4096, None, None, None, CachingConfig::default()); assert_eq!(result.messages.len(), 3); assert_eq!(result.messages[0].role(), &ConversationRole::User); @@ -149,6 +149,7 @@ fn test_system_prompt_separated_from_messages() { None, None, None, + CachingConfig::default(), ); assert_eq!(result.system.len(), 1); @@ -159,16 +160,16 @@ fn test_system_prompt_separated_from_messages() { #[test] fn test_empty_system_prompt_produces_empty_vec() { let result = - build_converse_request(vec![], Some("".to_string()), vec![], 4096, None, None, None); + build_converse_request(vec![], Some("".to_string()), vec![], 4096, None, None, None, CachingConfig::default()); assert!(result.system.is_empty()); - let result2 = build_converse_request(vec![], None, vec![], 4096, None, None, None); + let result2 = build_converse_request(vec![], None, vec![], 4096, None, None, None, CachingConfig::default()); assert!(result2.system.is_empty()); } #[test] fn test_empty_tools_produce_none_config() { - let result = build_converse_request(vec![], None, vec![], 4096, None, None, None); + let result = build_converse_request(vec![], None, vec![], 4096, None, None, None, CachingConfig::default()); assert!(result.tool_config.is_none()); } @@ -195,7 +196,7 @@ fn test_tool_definitions_produce_tool_config() { #[test] fn test_inference_config_max_tokens_only() { - let result = build_converse_request(vec![], None, vec![], 8192, None, None, None); + let result = build_converse_request(vec![], None, vec![], 8192, None, None, None, CachingConfig::default()); assert_eq!(result.inference_config.max_tokens(), Some(8192)); assert_eq!(result.inference_config.temperature(), None); assert_eq!(result.inference_config.top_p(), None); @@ -212,6 +213,7 @@ fn test_inference_config_all_params() { Some(0.7), Some(0.9), Some(vec!["STOP".to_string()]), + CachingConfig::default(), ); assert_eq!(result.inference_config.max_tokens(), Some(4096)); assert_eq!(result.inference_config.temperature(), Some(0.7)); @@ -233,7 +235,7 @@ fn test_multipart_content_produces_multiple_blocks() { ]), }]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, vec![], 4096, None, None, None, CachingConfig::default()); assert_eq!(result.messages[0].content().len(), 2); assert!(matches!( @@ -271,7 +273,7 @@ fn test_tool_result_after_tool_use_coalesced_into_user_message() { }, ]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, vec![], 4096, None, None, None, CachingConfig::default()); assert_eq!(result.messages.len(), 3); assert_eq!(result.messages[0].role(), &ConversationRole::User); @@ -282,3 +284,127 @@ fn test_tool_result_after_tool_use_coalesced_into_user_message() { ContentBlock::ToolResult(_) )); } + +#[test] +fn test_caching_disabled_no_cache_points() { + let config = CachingConfig { + enabled: false, + extended_ttl_requested: false, + }; + + let messages = vec![ + ConversationMessage { + role: MessageRole::User, + content: MessageContent::Text("first message".to_string()), + }, + ConversationMessage { + role: MessageRole::Assistant, + content: MessageContent::Text("response".to_string()), + }, + ConversationMessage { + role: MessageRole::User, + content: MessageContent::Text("second message".to_string()), + }, + ]; + + let tools = vec![ToolDefinition { + name: "test_tool".to_string(), + description: "Test tool".to_string(), + input_schema: json!({"type": "object", "properties": {}}), + }]; + + let result = build_converse_request( + messages, + Some("System prompt".to_string()), + tools, + 4096, + None, + None, + None, + config, + ); + + // Check that no cache points exist in messages + for msg in &result.messages { + for content in msg.content() { + assert!(!matches!(content, ContentBlock::CachePoint(_))); + } + } + + // Check that no cache points exist in system + for block in &result.system { + use aws_sdk_bedrockruntime::types::SystemContentBlock; + assert!(!matches!(block, SystemContentBlock::CachePoint(_))); + } + + // Check that no cache points exist in tools + if let Some(tool_config) = result.tool_config { + for tool in tool_config.tools() { + use aws_sdk_bedrockruntime::types::Tool; + assert!(!matches!(tool, Tool::CachePoint(_))); + } + } +} + +#[test] +fn test_caching_enabled_has_cache_points() { + let config = CachingConfig::default(); // enabled by default + + let messages = vec![ + ConversationMessage { + role: MessageRole::User, + content: MessageContent::Text("first message".to_string()), + }, + ConversationMessage { + role: MessageRole::Assistant, + content: MessageContent::Text("response".to_string()), + }, + ConversationMessage { + role: MessageRole::User, + content: MessageContent::Text("second message".to_string()), + }, + ]; + + let tools = vec![ToolDefinition { + name: "test_tool".to_string(), + description: "Test tool".to_string(), + input_schema: json!({"type": "object", "properties": {}}), + }]; + + let result = build_converse_request( + messages, + Some("System prompt".to_string()), + tools, + 4096, + None, + None, + None, + config, + ); + + // Check that cache point exists in second-to-last message (index 1 of 3) + let second_to_last_msg = &result.messages[1]; + let has_cache_point = second_to_last_msg + .content() + .iter() + .any(|c| matches!(c, ContentBlock::CachePoint(_))); + assert!(has_cache_point, "Second-to-last message should have cache point"); + + // Check that cache point exists in system + use aws_sdk_bedrockruntime::types::SystemContentBlock; + let has_system_cache = result + .system + .iter() + .any(|b| matches!(b, SystemContentBlock::CachePoint(_))); + assert!(has_system_cache, "System should have cache point"); + + // Check that cache point exists in tools + if let Some(tool_config) = result.tool_config { + use aws_sdk_bedrockruntime::types::Tool; + let has_tool_cache = tool_config + .tools() + .iter() + .any(|t| matches!(t, Tool::CachePoint(_))); + assert!(has_tool_cache, "Tools should have cache point"); + } +} diff --git a/app/src/ai/bedrock/external_config.rs b/app/src/ai/bedrock/external_config.rs index abfd0158..96d3bfd5 100644 --- a/app/src/ai/bedrock/external_config.rs +++ b/app/src/ai/bedrock/external_config.rs @@ -9,6 +9,10 @@ pub struct ExternalBedrockConfig { pub region: Option, pub models: Vec, pub auth_refresh_command: Option, + pub disable_prompt_caching: bool, + pub enable_prompt_caching_1h: bool, + pub anthropic_model: Option, + pub anthropic_small_fast_model: Option, } impl ExternalBedrockConfig { @@ -30,6 +34,10 @@ impl ExternalBedrockConfig { claude_config.models }, auth_refresh_command: claude_config.auth_refresh_command, + disable_prompt_caching: claude_config.disable_prompt_caching, + enable_prompt_caching_1h: claude_config.enable_prompt_caching_1h, + anthropic_model: claude_config.anthropic_model, + anthropic_small_fast_model: claude_config.anthropic_small_fast_model, } } @@ -83,11 +91,37 @@ fn parse_claude_code_config(path: PathBuf) -> ExternalBedrockConfig { let models = parse_claude_code_model_map(env); + let disable_prompt_caching = env + .get("DISABLE_PROMPT_CACHING") + .and_then(|v| v.as_str()) + .map(|s| s == "1") + .unwrap_or(false); + + let enable_prompt_caching_1h = env + .get("ENABLE_PROMPT_CACHING_1H") + .and_then(|v| v.as_str()) + .map(|s| s == "1") + .unwrap_or(false); + + let anthropic_model = env + .get("ANTHROPIC_MODEL") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let anthropic_small_fast_model = env + .get("ANTHROPIC_SMALL_FAST_MODEL") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + ExternalBedrockConfig { profile, region, models, auth_refresh_command, + disable_prompt_caching, + enable_prompt_caching_1h, + anthropic_model, + anthropic_small_fast_model, } } @@ -234,6 +268,10 @@ fn parse_opencode_config(path: PathBuf) -> ExternalBedrockConfig { region, models: Vec::new(), auth_refresh_command: None, + disable_prompt_caching: false, + enable_prompt_caching_1h: false, + anthropic_model: None, + anthropic_small_fast_model: None, } } diff --git a/app/src/ai/bedrock/external_config_tests.rs b/app/src/ai/bedrock/external_config_tests.rs index 355e2850..9dc55131 100644 --- a/app/src/ai/bedrock/external_config_tests.rs +++ b/app/src/ai/bedrock/external_config_tests.rs @@ -48,6 +48,73 @@ fn test_parse_claude_code_config_extracts_model_map() { assert!(config.models[0].vision_supported); } +#[test] +fn test_parse_claude_code_config_extracts_caching_flags() { + let mut file = NamedTempFile::new().unwrap(); + write!( + file, + r#"{{ + "env": {{ + "AWS_PROFILE": "test", + "AWS_REGION": "us-east-1", + "DISABLE_PROMPT_CACHING": "1", + "ENABLE_PROMPT_CACHING_1H": "1" + }} + }}"# + ) + .unwrap(); + + let config = parse_claude_code_config(file.path().to_path_buf()); + assert!(config.disable_prompt_caching); + assert!(config.enable_prompt_caching_1h); +} + +#[test] +fn test_parse_claude_code_config_caching_flags_default_false() { + let mut file = NamedTempFile::new().unwrap(); + write!( + file, + r#"{{ + "env": {{ + "AWS_PROFILE": "test", + "AWS_REGION": "us-east-1" + }} + }}"# + ) + .unwrap(); + + let config = parse_claude_code_config(file.path().to_path_buf()); + assert!(!config.disable_prompt_caching); + assert!(!config.enable_prompt_caching_1h); +} + +#[test] +fn test_parse_claude_code_config_extracts_model_env_vars() { + let mut file = NamedTempFile::new().unwrap(); + write!( + file, + r#"{{ + "env": {{ + "AWS_PROFILE": "test", + "AWS_REGION": "us-east-1", + "ANTHROPIC_MODEL": "us.anthropic.claude-sonnet-4-5-20250929-v1:0", + "ANTHROPIC_SMALL_FAST_MODEL": "us.anthropic.claude-haiku-4-5-20251001-v1:0" + }} + }}"# + ) + .unwrap(); + + let config = parse_claude_code_config(file.path().to_path_buf()); + assert_eq!( + config.anthropic_model, + Some("us.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string()) + ); + assert_eq!( + config.anthropic_small_fast_model, + Some("us.anthropic.claude-haiku-4-5-20251001-v1:0".to_string()) + ); +} + #[test] fn test_parse_claude_code_config_missing_file() { let config = parse_claude_code_config(PathBuf::from("/nonexistent/path/settings.json")); diff --git a/app/src/ai/bedrock/mod.rs b/app/src/ai/bedrock/mod.rs index 546070f6..885614b9 100644 --- a/app/src/ai/bedrock/mod.rs +++ b/app/src/ai/bedrock/mod.rs @@ -6,6 +6,7 @@ pub mod external_config; pub mod models; pub mod request_translator; pub mod response_translator; +pub mod settings_view; pub mod translator; #[cfg(test)] diff --git a/app/src/ai/bedrock/settings_view.rs b/app/src/ai/bedrock/settings_view.rs new file mode 100644 index 00000000..fa01f031 --- /dev/null +++ b/app/src/ai/bedrock/settings_view.rs @@ -0,0 +1,200 @@ +use crate::ai::bedrock::convert::CachingConfig; +use crate::ai::bedrock::external_config::ExternalBedrockConfig; +use crate::appearance::Appearance; +use crate::ui_components::blended_colors; +use galaxyui::{ + elements::{ + Container, CornerRadius, CrossAxisAlignment, Flex, ParentElement, Radius, Text, + }, + AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, +}; + +pub struct SettingsView { + external_config: ExternalBedrockConfig, + caching_config: CachingConfig, +} + +impl SettingsView { + pub fn new() -> Self { + let external_config = ExternalBedrockConfig::load(); + let caching_config = CachingConfig::from_external_config(&external_config); + Self { + external_config, + caching_config, + } + } +} + +impl View for SettingsView { + fn ui_name() -> &'static str { + "SettingsView" + } + + fn render(&self, app: &AppContext) -> Box { + let appearance = Appearance::as_ref(app); + let theme = appearance.theme(); + let font_size = appearance.ui_font_size(); + let text_color = blended_colors::text_main(theme, theme.surface_2()); + let label_color = blended_colors::text_sub(theme, theme.surface_2()); + + let mut column = Flex::column() + .with_cross_axis_alignment(CrossAxisAlignment::Stretch) + .with_spacing(8.0); + + // Header + column = column.with_child( + Text::new( + "Galaxy Bedrock Settings", + appearance.ui_font_family(), + font_size + 2.0, + ) + .with_color(text_color) + .finish(), + ); + + // Separator + column = column.with_child( + Text::new( + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", + appearance.ui_font_family(), + font_size, + ) + .with_color(label_color) + .finish(), + ); + + // AWS Configuration + if let Some(profile) = &self.external_config.profile { + column = column.with_child( + Text::new( + format!("AWS Profile: {}", profile), + appearance.ui_font_family(), + font_size, + ) + .with_color(text_color) + .finish(), + ); + } + + if let Some(region) = &self.external_config.region { + column = column.with_child( + Text::new( + format!("AWS Region: {}", region), + appearance.ui_font_family(), + font_size, + ) + .with_color(text_color) + .finish(), + ); + } + + // Model Configuration + if let Some(model) = &self.external_config.anthropic_model { + column = column.with_child( + Text::new( + format!("Primary Model: {}", model), + appearance.ui_font_family(), + font_size, + ) + .with_color(text_color) + .soft_wrap(true) + .finish(), + ); + } + + if let Some(model) = &self.external_config.anthropic_small_fast_model { + column = column.with_child( + Text::new( + format!("Small/Fast Model: {}", model), + appearance.ui_font_family(), + font_size, + ) + .with_color(text_color) + .soft_wrap(true) + .finish(), + ); + } + + // Caching Configuration + let caching_status = if self.caching_config.enabled { + "✓ Enabled" + } else { + "✗ Disabled" + }; + column = column.with_child( + Text::new( + format!("Prompt Caching: {}", caching_status), + appearance.ui_font_family(), + font_size, + ) + .with_color(text_color) + .finish(), + ); + + if self.caching_config.extended_ttl_requested { + column = column.with_child( + Text::new( + "Extended 1h TTL: ✓ Requested", + appearance.ui_font_family(), + font_size, + ) + .with_color(text_color) + .finish(), + ); + } + + // Additional Models + if !self.external_config.models.is_empty() { + column = column.with_child( + Text::new( + format!("\nAdditional models: {}", self.external_config.models.len()), + appearance.ui_font_family(), + font_size, + ) + .with_color(label_color) + .finish(), + ); + } + + // Auth Refresh Command + if let Some(cmd) = &self.external_config.auth_refresh_command { + column = column.with_child( + Text::new( + format!("\nAuth Refresh: {}", cmd), + appearance.ui_font_family(), + font_size - 1.0, + ) + .with_color(label_color) + .soft_wrap(true) + .finish(), + ); + } + + // Help text + column = column.with_child( + Text::new( + "\nSettings loaded from ~/.claude/settings.json", + appearance.ui_font_family(), + font_size - 1.0, + ) + .with_color(label_color) + .finish(), + ); + + Container::new(column.finish()) + .with_uniform_padding(12.0) + .with_background(theme.surface_2()) + .with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.0))) + .finish() + } +} + +impl Entity for SettingsView { + type Event = (); +} + +impl TypedActionView for SettingsView { + type Action = (); + + fn handle_action(&mut self, _action: &Self::Action, _ctx: &mut ViewContext) {} +} diff --git a/app/src/search/slash_command_menu/static_commands/commands.rs b/app/src/search/slash_command_menu/static_commands/commands.rs index 86c4c53c..38c6e622 100644 --- a/app/src/search/slash_command_menu/static_commands/commands.rs +++ b/app/src/search/slash_command_menu/static_commands/commands.rs @@ -396,6 +396,15 @@ pub const CONTEXT: StaticCommand = StaticCommand { argument: None, }; +pub const SETTINGS: StaticCommand = StaticCommand { + name: "/settings", + description: "Show Galaxy configuration (Bedrock, models, caching)", + icon_path: "bundled/svg/settings-06.svg", + availability: Availability::ALWAYS, + auto_enter_ai_mode: false, + argument: None, +}; + pub const CONVERSATIONS: StaticCommand = StaticCommand { name: "/conversations", description: "Open conversation history", @@ -531,6 +540,7 @@ fn all_commands() -> Vec { CONVERSATIONS, EXPORT_TO_CLIPBOARD, MODEL.clone(), + SETTINGS, ]; if FeatureFlag::LocalDockerSandbox.is_enabled() { diff --git a/app/src/terminal/input/slash_commands/mod.rs b/app/src/terminal/input/slash_commands/mod.rs index a375e98c..55b34078 100644 --- a/app/src/terminal/input/slash_commands/mod.rs +++ b/app/src/terminal/input/slash_commands/mod.rs @@ -712,6 +712,9 @@ impl Input { ctx.dispatch_typed_action(&TerminalAction::ToggleContextView); } } + _settings if command.name == commands::SETTINGS.name => { + ctx.dispatch_typed_action(&TerminalAction::ToggleSettingsView); + } _fork if command.name == commands::FORK.name => { let Some(conversation_id) = self .ai_context_model diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index 19da5ad9..a604e480 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -2589,6 +2589,9 @@ pub struct TerminalView { /// View ID of the context window debug view, if visible. context_view_id: Option, + /// View ID of the settings view, if visible. + settings_view_id: Option, + // Whether the block onboarding view is active or not. block_onboarding_active: bool, @@ -4093,6 +4096,7 @@ impl TerminalView { rich_content_views: Vec::new(), usage_footer_view_ids: Default::default(), context_view_id: None, + settings_view_id: None, block_onboarding_active: false, onboarding_agentic_suggestions_block: None, onboarding_prompt_block: None, @@ -5792,6 +5796,36 @@ impl TerminalView { ctx.notify(); } + fn toggle_settings_view(&mut self, ctx: &mut ViewContext) { + use crate::ai::bedrock::settings_view::SettingsView; + + // If already showing, remove it + if let Some(view_id) = self.settings_view_id.take() { + let mut model = self.model.lock(); + model.block_list_mut().remove_rich_content(view_id); + drop(model); + self.rich_content_views.retain(|rc| rc.view_id() != view_id); + ctx.notify(); + return; + } + + // Create and insert the settings view + let settings_view = ctx.add_view(|_| SettingsView::new()); + let view_id = settings_view.id(); + self.settings_view_id = Some(view_id); + + self.insert_rich_content( + None, + settings_view, + None, + RichContentInsertionPosition::Append { + insert_below_long_running_block: true, + }, + ctx, + ); + ctx.notify(); + } + /// Returns true if the window is wide enough to auto-open side panels. pub fn can_auto_open_panel(&self) -> bool { self.size_info.pane_width_px().as_f32() > MINIMUM_WIDTH_TO_AUTO_OPEN_PANE @@ -24532,6 +24566,7 @@ impl TypedActionView for TerminalView { | ExecuteRewindFromInlineMenu { .. } | ToggleUsageFooter | ToggleContextView + | ToggleSettingsView | RevealChildAgent { .. } | OpenCLIAgentRichInput | ToggleSessionRecording => Empty, @@ -25556,6 +25591,9 @@ impl TypedActionView for TerminalView { ToggleContextView => { self.toggle_context_view(ctx); } + ToggleSettingsView => { + self.toggle_settings_view(ctx); + } RevealChildAgent { conversation_id } => { ctx.emit(Event::RevealChildAgent { conversation_id: *conversation_id, diff --git a/app/src/terminal/view/action.rs b/app/src/terminal/view/action.rs index 9f4e949c..db3a685e 100644 --- a/app/src/terminal/view/action.rs +++ b/app/src/terminal/view/action.rs @@ -424,6 +424,8 @@ pub enum TerminalAction { ToggleUsageFooter, /// Toggle the context window debug view showing bedrock_message_history. ToggleContextView, + /// Toggle the settings view showing Galaxy Bedrock configuration. + ToggleSettingsView, /// Reveal a hidden child agent pane from the orchestrator status card. RevealChildAgent { conversation_id: AIConversationId, @@ -705,6 +707,7 @@ impl fmt::Debug for TerminalAction { AwsCliNotInstalledBanner(action) => write!(f, "AwsCliNotInstalledBanner({action:?})"), ToggleUsageFooter => write!(f, "ToggleUsageFooter"), ToggleContextView => write!(f, "ToggleContextView"), + ToggleSettingsView => write!(f, "ToggleSettingsView"), RevealChildAgent { .. } => write!(f, "RevealChildAgent"), ToggleSessionRecording => write!(f, "ToggleSessionRecording"), OpenCLIAgentRichInput => write!(f, "OpenCLIAgentRichInput"),