agent_ui: Set max-width for thread view content (#52730)

This PR adds a configurable max-width to the agent panel. This will be
particularly useful when opting into an agentic-first layout where the
thread will be at the center of the UI (with the panel most likely
full-screen'ed, which is why I'm also adding here the button to make it
full screen in the toolbar). The default max-width is 850, which is a
bit bigger than the one generally considered as a standard (~66
characters wide, which usually sums up to 750 pixels).

Release Notes:

- Agent: Added a max-width to the thread view for better readability,
particularly when the panel is zoomed in.
This commit is contained in:
Danilo Leal 2026-04-07 09:13:05 -03:00 committed by GitHub
parent eaf14d028a
commit 0bde5094f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 204 additions and 158 deletions

View file

@ -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.

View file

@ -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,

View file

@ -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<LanguageModelSelection>,
pub inline_assistant_model: Option<LanguageModelSelection>,
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,

View file

@ -3186,17 +3186,11 @@ impl AgentPanel {
fn render_panel_options_menu(
&self,
window: &mut Window,
_window: &mut Window,
cx: &mut Context<Self>,
) -> 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<Self>) -> Option<AnyElement> {

View file

@ -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,

View file

@ -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),
)
.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()
.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(
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::<AgentV2FeatureFlag>() && !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))

View file

@ -128,6 +128,12 @@ pub struct AgentSettingsContent {
/// Default: 320
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
pub default_height: Option<f32>,
/// 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<f32>,
/// The default model to use when creating new chats and for other features when a specific model is not specified.
pub default_model: Option<LanguageModelSelection>,
/// Favorite models to show at the top of the model selector.

View file

@ -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,
}),
]
}