diff --git a/assets/settings/default.json b/assets/settings/default.json index 63e906e3b11206fc458f8d7353f3ecba0abeb825..a32e1b27aee08bf2676922fea3790a99b7d7844b 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -965,6 +965,9 @@ "default_width": 640, // Default height when the agent panel is docked to the bottom. "default_height": 320, + // Maximum content width when the agent panel is wider than this value. + // Content will be centered within the panel. + "max_content_width": 850, // The default model to use when creating new threads. "default_model": { // The provider to use. diff --git a/crates/agent/src/tool_permissions.rs b/crates/agent/src/tool_permissions.rs index 58e779da59aef176464839ed6f2d6a5c16e4bc12..ff9e735b6c4181588ed5cddbd6dada7fbae5f18f 100644 --- a/crates/agent/src/tool_permissions.rs +++ b/crates/agent/src/tool_permissions.rs @@ -574,6 +574,7 @@ mod tests { flexible: true, default_width: px(300.), default_height: px(600.), + max_content_width: px(850.), default_model: None, inline_assistant_model: None, inline_assistant_use_streaming_tools: false, diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 0c68d2f25d54f966d1cc0a93476457bbba79c959..5d6dca9322482daecf7525f79ead63b4471b7a53 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -154,6 +154,7 @@ pub struct AgentSettings { pub sidebar_side: SidebarDockPosition, pub default_width: Pixels, pub default_height: Pixels, + pub max_content_width: Pixels, pub default_model: Option, pub inline_assistant_model: Option, pub inline_assistant_use_streaming_tools: bool, @@ -600,6 +601,7 @@ impl Settings for AgentSettings { sidebar_side: agent.sidebar_side.unwrap(), default_width: px(agent.default_width.unwrap()), default_height: px(agent.default_height.unwrap()), + max_content_width: px(agent.max_content_width.unwrap()), flexible: agent.flexible.unwrap(), default_model: Some(agent.default_model.unwrap()), inline_assistant_model: agent.inline_assistant_model, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 8f456e0e955b823a5bbaf2815df3b409441bb0af..01b897fc63da76247b5624f8316ea06b2c1f85e5 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -3186,17 +3186,11 @@ impl AgentPanel { fn render_panel_options_menu( &self, - window: &mut Window, + _window: &mut Window, cx: &mut Context, ) -> impl IntoElement { let focus_handle = self.focus_handle(cx); - let full_screen_label = if self.is_zoomed(window, cx) { - "Disable Full Screen" - } else { - "Enable Full Screen" - }; - let conversation_view = match &self.active_view { ActiveView::AgentThread { conversation_view } => Some(conversation_view.clone()), _ => None, @@ -3272,8 +3266,7 @@ impl AgentPanel { .action("Profiles", Box::new(ManageProfiles::default())) .action("Settings", Box::new(OpenSettings)) .separator() - .action("Toggle Threads Sidebar", Box::new(ToggleWorkspaceSidebar)) - .action(full_screen_label, Box::new(ToggleZoom)); + .action("Toggle Threads Sidebar", Box::new(ToggleWorkspaceSidebar)); if has_auth_methods { menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent)) @@ -3709,21 +3702,37 @@ impl AgentPanel { ); let is_full_screen = self.is_zoomed(window, cx); + let full_screen_button = if is_full_screen { + IconButton::new("disable-full-screen", IconName::Minimize) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| Tooltip::for_action("Disable Full Screen", &ToggleZoom, cx)) + .on_click(cx.listener(move |this, _, window, cx| { + this.toggle_zoom(&ToggleZoom, window, cx); + })) + } else { + IconButton::new("enable-full-screen", IconName::Maximize) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| Tooltip::for_action("Enable Full Screen", &ToggleZoom, cx)) + .on_click(cx.listener(move |this, _, window, cx| { + this.toggle_zoom(&ToggleZoom, window, cx); + })) + }; let use_v2_empty_toolbar = has_v2_flag && is_empty_state && !is_in_history_or_config; + let max_content_width = AgentSettings::get_global(cx).max_content_width; + let base_container = h_flex() - .id("agent-panel-toolbar") - .h(Tab::container_height(cx)) - .max_w_full() + .size_full() + // TODO: This is only until we remove Agent settings from the panel. + .when(!is_in_history_or_config, |this| { + this.max_w(max_content_width).mx_auto() + }) .flex_none() .justify_between() - .gap_2() - .bg(cx.theme().colors().tab_bar_background) - .border_b_1() - .border_color(cx.theme().colors().border); + .gap_2(); - if use_v2_empty_toolbar { + let toolbar_content = if use_v2_empty_toolbar { let (chevron_icon, icon_color, label_color) = if self.new_thread_menu_handle.is_deployed() { (IconName::ChevronUp, Color::Accent, Color::Accent) @@ -3805,20 +3814,7 @@ impl AgentPanel { cx, )) }) - .when(is_full_screen, |this| { - this.child( - IconButton::new("disable-full-screen", IconName::Minimize) - .icon_size(IconSize::Small) - .tooltip(move |_, cx| { - Tooltip::for_action("Disable Full Screen", &ToggleZoom, cx) - }) - .on_click({ - cx.listener(move |_, _, window, cx| { - window.dispatch_action(ToggleZoom.boxed_clone(), cx); - }) - }), - ) - }) + .child(full_screen_button) .child(self.render_panel_options_menu(window, cx)), ) .into_any_element() @@ -3871,24 +3867,21 @@ impl AgentPanel { cx, )) }) - .when(is_full_screen, |this| { - this.child( - IconButton::new("disable-full-screen", IconName::Minimize) - .icon_size(IconSize::Small) - .tooltip(move |_, cx| { - Tooltip::for_action("Disable Full Screen", &ToggleZoom, cx) - }) - .on_click({ - cx.listener(move |_, _, window, cx| { - window.dispatch_action(ToggleZoom.boxed_clone(), cx); - }) - }), - ) - }) + .child(full_screen_button) .child(self.render_panel_options_menu(window, cx)), ) .into_any_element() - } + }; + + h_flex() + .id("agent-panel-toolbar") + .h(Tab::container_height(cx)) + .flex_shrink_0() + .max_w_full() + .bg(cx.theme().colors().tab_bar_background) + .border_b_1() + .border_color(cx.theme().colors().border) + .child(toolbar_content) } fn render_worktree_creation_status(&self, cx: &mut Context) -> Option { diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 9daa7c6cd83c276aa99adc9e3aae3e6c82c5ba88..58b52d9ea2eb10a4f7f483402b98c4be4b08924f 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -742,6 +742,7 @@ mod tests { flexible: true, default_width: px(300.), default_height: px(600.), + max_content_width: px(850.), default_model: None, inline_assistant_model: None, inline_assistant_use_streaming_tools: false, diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index ff3dab1170064e058c0ebb44505c0906349517ee..27ebadade8047db5f2b4de63c5c3731708d9af59 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -3014,14 +3014,12 @@ impl ThreadView { let is_done = thread.read(cx).status() == ThreadStatus::Idle; let is_canceled_or_failed = self.is_subagent_canceled_or_failed(cx); + let max_content_width = AgentSettings::get_global(cx).max_content_width; + Some( h_flex() - .h(Tab::container_height(cx)) - .pl_2() - .pr_1p5() .w_full() - .justify_between() - .gap_1() + .h(Tab::container_height(cx)) .border_b_1() .when(is_done && is_canceled_or_failed, |this| { this.border_dashed() @@ -3030,50 +3028,61 @@ impl ThreadView { .bg(cx.theme().colors().editor_background.opacity(0.2)) .child( h_flex() - .flex_1() - .gap_2() + .size_full() + .max_w(max_content_width) + .mx_auto() + .pl_2() + .pr_1() + .flex_shrink_0() + .justify_between() + .gap_1() .child( - Icon::new(IconName::ForwardArrowUp) - .size(IconSize::Small) - .color(Color::Muted), + h_flex() + .flex_1() + .gap_2() + .child( + Icon::new(IconName::ForwardArrowUp) + .size(IconSize::Small) + .color(Color::Muted), + ) + .child(self.title_editor.clone()) + .when(is_done && is_canceled_or_failed, |this| { + this.child(Icon::new(IconName::Close).color(Color::Error)) + }) + .when(is_done && !is_canceled_or_failed, |this| { + this.child(Icon::new(IconName::Check).color(Color::Success)) + }), ) - .child(self.title_editor.clone()) - .when(is_done && is_canceled_or_failed, |this| { - this.child(Icon::new(IconName::Close).color(Color::Error)) - }) - .when(is_done && !is_canceled_or_failed, |this| { - this.child(Icon::new(IconName::Check).color(Color::Success)) - }), - ) - .child( - h_flex() - .gap_0p5() - .when(!is_done, |this| { - this.child( - IconButton::new("stop_subagent", IconName::Stop) - .icon_size(IconSize::Small) - .icon_color(Color::Error) - .tooltip(Tooltip::text("Stop Subagent")) - .on_click(move |_, _, cx| { - thread.update(cx, |thread, cx| { - thread.cancel(cx).detach(); - }); - }), - ) - }) .child( - IconButton::new("minimize_subagent", IconName::Minimize) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Minimize Subagent")) - .on_click(move |_, window, cx| { - let _ = server_view.update(cx, |server_view, cx| { - server_view.navigate_to_session( - parent_session_id.clone(), - window, - cx, - ); - }); - }), + h_flex() + .gap_0p5() + .when(!is_done, |this| { + this.child( + IconButton::new("stop_subagent", IconName::Stop) + .icon_size(IconSize::Small) + .icon_color(Color::Error) + .tooltip(Tooltip::text("Stop Subagent")) + .on_click(move |_, _, cx| { + thread.update(cx, |thread, cx| { + thread.cancel(cx).detach(); + }); + }), + ) + }) + .child( + IconButton::new("minimize_subagent", IconName::Dash) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Minimize Subagent")) + .on_click(move |_, window, cx| { + let _ = server_view.update(cx, |server_view, cx| { + server_view.navigate_to_session( + parent_session_id.clone(), + window, + cx, + ); + }); + }), + ), ), ), ) @@ -3099,6 +3108,8 @@ impl ThreadView { (IconName::Maximize, "Expand Message Editor") }; + let max_content_width = AgentSettings::get_global(cx).max_content_width; + v_flex() .on_action(cx.listener(Self::expand_message_editor)) .p_2() @@ -3113,73 +3124,80 @@ impl ThreadView { }) .child( v_flex() - .relative() - .size_full() - .when(v2_empty_state, |this| this.flex_1()) - .pt_1() - .pr_2p5() - .child(self.message_editor.clone()) - .when(!v2_empty_state, |this| { - this.child( - h_flex() - .absolute() - .top_0() - .right_0() - .opacity(0.5) - .hover(|this| this.opacity(1.0)) - .child( - IconButton::new("toggle-height", expand_icon) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip({ - move |_window, cx| { - Tooltip::for_action_in( - expand_tooltip, - &ExpandMessageEditor, - &focus_handle, - cx, - ) - } - }) - .on_click(cx.listener(|this, _, window, cx| { - this.expand_message_editor( - &ExpandMessageEditor, - window, - cx, - ); - })), - ), - ) - }), - ) - .child( - h_flex() - .flex_none() - .flex_wrap() - .justify_between() + .flex_1() + .w_full() + .max_w(max_content_width) + .mx_auto() .child( - h_flex() - .gap_0p5() - .child(self.render_add_context_button(cx)) - .child(self.render_follow_toggle(cx)) - .children(self.render_fast_mode_control(cx)) - .children(self.render_thinking_control(cx)), + v_flex() + .relative() + .size_full() + .when(v2_empty_state, |this| this.flex_1()) + .pt_1() + .pr_2p5() + .child(self.message_editor.clone()) + .when(!v2_empty_state, |this| { + this.child( + h_flex() + .absolute() + .top_0() + .right_0() + .opacity(0.5) + .hover(|this| this.opacity(1.0)) + .child( + IconButton::new("toggle-height", expand_icon) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .tooltip({ + move |_window, cx| { + Tooltip::for_action_in( + expand_tooltip, + &ExpandMessageEditor, + &focus_handle, + cx, + ) + } + }) + .on_click(cx.listener(|this, _, window, cx| { + this.expand_message_editor( + &ExpandMessageEditor, + window, + cx, + ); + })), + ), + ) + }), ) .child( h_flex() - .gap_1() - .children(self.render_token_usage(cx)) - .children(self.profile_selector.clone()) - .map(|this| { - // Either config_options_view OR (mode_selector + model_selector) - match self.config_options_view.clone() { - Some(config_view) => this.child(config_view), - None => this - .children(self.mode_selector.clone()) - .children(self.model_selector.clone()), - } - }) - .child(self.render_send_button(cx)), + .flex_none() + .flex_wrap() + .justify_between() + .child( + h_flex() + .gap_0p5() + .child(self.render_add_context_button(cx)) + .child(self.render_follow_toggle(cx)) + .children(self.render_fast_mode_control(cx)) + .children(self.render_thinking_control(cx)), + ) + .child( + h_flex() + .gap_1() + .children(self.render_token_usage(cx)) + .children(self.profile_selector.clone()) + .map(|this| { + // Either config_options_view OR (mode_selector + model_selector) + match self.config_options_view.clone() { + Some(config_view) => this.child(config_view), + None => this + .children(self.mode_selector.clone()) + .children(self.model_selector.clone()), + } + }) + .child(self.render_send_button(cx)), + ), ), ) .into_any() @@ -8559,8 +8577,12 @@ impl Render for ThreadView { let has_messages = self.list_state.item_count() > 0; let v2_empty_state = cx.has_flag::() && !has_messages; + let max_content_width = AgentSettings::get_global(cx).max_content_width; + let conversation = v_flex() - .when(!v2_empty_state, |this| this.flex_1()) + .mx_auto() + .max_w(max_content_width) + .when(!v2_empty_state, |this| this.flex_1().size_full()) .map(|this| { let this = this.when(self.resumed_without_history, |this| { this.child(Self::render_resume_notice(cx)) diff --git a/crates/settings_content/src/agent.rs b/crates/settings_content/src/agent.rs index 5b1b3c014f8c538cb0dff506e05d84a80dc863d1..7a9a1ddb16ac91f90f73e17b3972cd31536d7a66 100644 --- a/crates/settings_content/src/agent.rs +++ b/crates/settings_content/src/agent.rs @@ -128,6 +128,12 @@ pub struct AgentSettingsContent { /// Default: 320 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_height: Option, + /// Maximum content width in pixels for the agent panel. Content will be + /// centered when the panel is wider than this value. + /// + /// Default: 850 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] + pub max_content_width: Option, /// The default model to use when creating new chats and for other features when a specific model is not specified. pub default_model: Option, /// Favorite models to show at the top of the model selector. diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 9978832c05bb29c97f118fccbe301214d81fa0c6..259ee2cf261f9e435a5431ddf3c470640daf41f9 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -5737,7 +5737,7 @@ fn panels_page() -> SettingsPage { ] } - fn agent_panel_section() -> [SettingsPageItem; 6] { + fn agent_panel_section() -> [SettingsPageItem; 7] { [ SettingsPageItem::SectionHeader("Agent Panel"), SettingsPageItem::SettingItem(SettingItem { @@ -5812,6 +5812,24 @@ fn panels_page() -> SettingsPage { metadata: None, files: USER, }), + SettingsPageItem::SettingItem(SettingItem { + title: "Agent Panel Max Content Width", + description: "Maximum content width in pixels. Content will be centered when the panel is wider than this value.", + field: Box::new(SettingField { + json_path: Some("agent.max_content_width"), + pick: |settings_content| { + settings_content.agent.as_ref()?.max_content_width.as_ref() + }, + write: |settings_content, value| { + settings_content + .agent + .get_or_insert_default() + .max_content_width = value; + }, + }), + metadata: None, + files: USER, + }), ] }