Allow the agent panel font size to be customized (#29954)

You can set `agent_font_size` as a top-level settings key. You can also
use `zed::IncreaseBufferFontSize` and `zed::DecreaseBufferFontSize` and
`zed::ResetBufferFontSize` the agent panel is focused via the standard
bindings to adjust the agent font size. In the future, it might make
sense to rename these actions to be more general since "buffer" is now a
bit of a misnomer. 🍐'd with @mikayla-maki

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
Nathan Sobo 2025-05-05 16:13:14 -06:00 committed by Joseph T. Lyons
parent 51bfe9df8a
commit 31419db05f
6 changed files with 310 additions and 189 deletions

View File

@ -67,6 +67,8 @@
"ui_font_weight": 400,
// The default font size for text in the UI
"ui_font_size": 16,
// The default font size for text in the agent panel
"agent_font_size": 16,
// How much to fade out unused code.
"unnecessary_code_fade": 0.3,
// Active pane styling settings.

View File

@ -35,7 +35,7 @@ use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
use project::{ProjectEntryId, ProjectItem as _};
use rope::Point;
use settings::{Settings as _, update_settings_file};
use settings::{Settings as _, SettingsStore, update_settings_file};
use std::ffi::OsStr;
use std::path::Path;
use std::rc::Rc;
@ -43,6 +43,7 @@ use std::sync::Arc;
use std::time::Duration;
use text::ToPoint;
use theme::ThemeSettings;
use ui::utils::WithRemSize;
use ui::{
Disclosure, IconButton, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
Tooltip, prelude::*,
@ -764,6 +765,7 @@ impl ActiveThread {
cx.observe(&thread, |_, _, cx| cx.notify()),
cx.subscribe_in(&thread, window, Self::handle_thread_event),
cx.subscribe(&thread_store, Self::handle_rules_loading_error),
cx.observe_global::<SettingsStore>(|_, cx| cx.notify()),
];
let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), {
@ -1689,12 +1691,14 @@ impl ActiveThread {
fn render_edit_message_editor(
&self,
state: &EditingMessageState,
window: &mut Window,
_window: &mut Window,
cx: &Context<Self>,
) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let font_size = TextSize::Small.rems(cx);
let line_height = font_size.to_pixels(window.rem_size()) * 1.75;
let font_size = TextSize::Small
.rems(cx)
.to_pixels(settings.agent_font_size(cx));
let line_height = font_size * 1.75;
let colors = cx.theme().colors();
@ -2061,185 +2065,202 @@ impl ActiveThread {
let panel_background = cx.theme().colors().panel_background;
v_flex()
.w_full()
.map(|parent| {
if let Some(checkpoint) = checkpoint.filter(|_| is_generating) {
let mut is_pending = false;
let mut error = None;
if let Some(last_restore_checkpoint) =
self.thread.read(cx).last_restore_checkpoint()
{
if last_restore_checkpoint.message_id() == message_id {
match last_restore_checkpoint {
LastRestoreCheckpoint::Pending { .. } => is_pending = true,
LastRestoreCheckpoint::Error { error: err, .. } => {
error = Some(err.clone());
WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
.size_full()
.child(
v_flex()
.w_full()
.map(|parent| {
if let Some(checkpoint) = checkpoint.filter(|_| is_generating) {
let mut is_pending = false;
let mut error = None;
if let Some(last_restore_checkpoint) =
self.thread.read(cx).last_restore_checkpoint()
{
if last_restore_checkpoint.message_id() == message_id {
match last_restore_checkpoint {
LastRestoreCheckpoint::Pending { .. } => is_pending = true,
LastRestoreCheckpoint::Error { error: err, .. } => {
error = Some(err.clone());
}
}
}
}
}
}
let restore_checkpoint_button =
Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
.icon(if error.is_some() {
IconName::XCircle
} else {
IconName::Undo
})
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::Start)
.icon_color(if error.is_some() {
Some(Color::Error)
} else {
None
})
.label_size(LabelSize::XSmall)
.disabled(is_pending)
.on_click(cx.listener(move |this, _, _window, cx| {
this.thread.update(cx, |thread, cx| {
thread
.restore_checkpoint(checkpoint.clone(), cx)
.detach_and_log_err(cx);
});
}));
let restore_checkpoint_button =
Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
.icon(if error.is_some() {
IconName::XCircle
} else {
IconName::Undo
})
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::Start)
.icon_color(if error.is_some() {
Some(Color::Error)
} else {
None
})
.label_size(LabelSize::XSmall)
.disabled(is_pending)
.on_click(cx.listener(move |this, _, _window, cx| {
this.thread.update(cx, |thread, cx| {
thread
.restore_checkpoint(checkpoint.clone(), cx)
.detach_and_log_err(cx);
});
}));
let restore_checkpoint_button = if is_pending {
restore_checkpoint_button
.with_animation(
("pulsating-restore-checkpoint-button", ix),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.alpha(delta),
let restore_checkpoint_button = if is_pending {
restore_checkpoint_button
.with_animation(
("pulsating-restore-checkpoint-button", ix),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.alpha(delta),
)
.into_any_element()
} else if let Some(error) = error {
restore_checkpoint_button
.tooltip(Tooltip::text(error.to_string()))
.into_any_element()
} else {
restore_checkpoint_button.into_any_element()
};
parent.child(
h_flex()
.pt_2p5()
.px_2p5()
.w_full()
.gap_1()
.child(ui::Divider::horizontal())
.child(restore_checkpoint_button)
.child(ui::Divider::horizontal()),
)
.into_any_element()
} else if let Some(error) = error {
restore_checkpoint_button
.tooltip(Tooltip::text(error.to_string()))
.into_any_element()
} else {
restore_checkpoint_button.into_any_element()
};
parent.child(
h_flex()
.pt_2p5()
.px_2p5()
.w_full()
.gap_1()
.child(ui::Divider::horizontal())
.child(restore_checkpoint_button)
.child(ui::Divider::horizontal()),
)
} else {
parent
}
})
.when(is_first_message, |parent| {
parent.child(self.render_rules_item(cx))
})
.child(styled_message)
.when(is_generating && is_last_message, |this| {
this.child(
h_flex()
.h_8()
.mt_2()
.mb_4()
.ml_4()
.py_1p5()
.when_some(loading_dots, |this, loading_dots| this.child(loading_dots)),
)
})
.when(show_feedback, move |parent| {
parent.child(feedback_items).when_some(
self.open_feedback_editors.get(&message_id),
move |parent, feedback_editor| {
let focus_handle = feedback_editor.focus_handle(cx);
parent.child(
v_flex()
.key_context("AgentFeedbackMessageEditor")
.on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
this.open_feedback_editors.remove(&message_id);
cx.notify();
}))
.on_action(cx.listener(move |this, _: &menu::Confirm, _, cx| {
this.submit_feedback_message(message_id, cx);
cx.notify();
}))
.on_action(cx.listener(Self::confirm_editing_message))
.mb_2()
.mx_4()
.p_2()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.child(feedback_editor.clone())
.child(
h_flex()
.gap_1()
.justify_end()
.child(
Button::new("dismiss-feedback-message", "Cancel")
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(
move |this, _, _window, cx| {
this.open_feedback_editors
.remove(&message_id);
cx.notify();
},
)),
)
.child(
Button::new(
"submit-feedback-message",
"Share Feedback",
)
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(
cx.listener(move |this, _, _window, cx| {
this.submit_feedback_message(message_id, cx);
cx.notify()
}),
),
),
),
} else {
parent
}
})
.when(is_first_message, |parent| {
parent.child(self.render_rules_item(cx))
})
.child(styled_message)
.when(is_generating && is_last_message, |this| {
this.child(
h_flex()
.h_8()
.mt_2()
.mb_4()
.ml_4()
.py_1p5()
.when_some(loading_dots, |this, loading_dots| {
this.child(loading_dots)
}),
)
},
)
})
.when(after_editing_message, |parent| {
// Backdrop to dim out the whole thread below the editing user message
parent.relative().child(
div()
.occlude()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8),
)
})
})
.when(show_feedback, move |parent| {
parent.child(feedback_items).when_some(
self.open_feedback_editors.get(&message_id),
move |parent, feedback_editor| {
let focus_handle = feedback_editor.focus_handle(cx);
parent.child(
v_flex()
.key_context("AgentFeedbackMessageEditor")
.on_action(cx.listener(
move |this, _: &menu::Cancel, _, cx| {
this.open_feedback_editors.remove(&message_id);
cx.notify();
},
))
.on_action(cx.listener(
move |this, _: &menu::Confirm, _, cx| {
this.submit_feedback_message(message_id, cx);
cx.notify();
},
))
.on_action(cx.listener(Self::confirm_editing_message))
.mb_2()
.mx_4()
.p_2()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.child(feedback_editor.clone())
.child(
h_flex()
.gap_1()
.justify_end()
.child(
Button::new(
"dismiss-feedback-message",
"Cancel",
)
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(
move |this, _, _window, cx| {
this.open_feedback_editors
.remove(&message_id);
cx.notify();
},
)),
)
.child(
Button::new(
"submit-feedback-message",
"Share Feedback",
)
.style(ButtonStyle::Tinted(
ui::TintColor::Accent,
))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(
move |this, _, _window, cx| {
this.submit_feedback_message(
message_id, cx,
);
cx.notify()
},
)),
),
),
)
},
)
})
.when(after_editing_message, |parent| {
// Backdrop to dim out the whole thread below the editing user message
parent.relative().child(
div()
.occlude()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8),
)
}),
)
.into_any()
}

View File

@ -33,6 +33,7 @@ use proto::Plan;
use rules_library::{RulesLibrary, open_rules_library};
use search::{BufferSearchBar, buffer_search::DivRegistrar};
use settings::{Settings, update_settings_file};
use theme::ThemeSettings;
use time::UtcOffset;
use ui::{
Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip,
@ -43,6 +44,7 @@ use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::{CollaboratorId, ToolbarItemView, Workspace};
use zed_actions::agent::OpenConfiguration;
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
use zed_llm_client::UsageLimit;
use crate::active_thread::{ActiveThread, ActiveThreadEvent};
@ -1032,6 +1034,54 @@ impl AssistantPanel {
self.assistant_dropdown_menu_handle.toggle(window, cx);
}
pub fn increase_font_size(
&mut self,
action: &IncreaseBufferFontSize,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.adjust_font_size(action.persist, px(1.0), cx);
}
pub fn decrease_font_size(
&mut self,
action: &DecreaseBufferFontSize,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.adjust_font_size(action.persist, px(-1.0), cx);
}
fn adjust_font_size(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
if persist {
update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, cx| {
let agent_font_size = ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
let _ = settings
.agent_font_size
.insert(theme::clamp_font_size(agent_font_size).0);
});
} else {
theme::adjust_agent_font_size(cx, |size| {
*size += delta;
});
}
}
pub fn reset_font_size(
&mut self,
action: &ResetBufferFontSize,
_: &mut Window,
cx: &mut Context<Self>,
) {
if action.persist {
update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
settings.agent_font_size = None;
});
} else {
theme::reset_agent_font_size(cx);
}
}
pub fn open_agent_diff(
&mut self,
_: &OpenAgentDiff,
@ -2371,6 +2421,9 @@ impl Render for AssistantPanel {
.on_action(cx.listener(Self::go_back))
.on_action(cx.listener(Self::toggle_navigation_menu))
.on_action(cx.listener(Self::toggle_options_menu))
.on_action(cx.listener(Self::increase_font_size))
.on_action(cx.listener(Self::decrease_font_size))
.on_action(cx.listener(Self::reset_font_size))
.child(self.render_toolbar(window, cx))
.map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent

View File

@ -509,13 +509,7 @@ impl MessageEditor {
}))
}
fn render_editor(
&self,
font_size: Rems,
line_height: Pixels,
window: &mut Window,
cx: &mut Context<Self>,
) -> Div {
fn render_editor(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
let thread = self.thread.read(cx);
let model = thread.configured_model();
@ -621,6 +615,10 @@ impl MessageEditor {
.when(is_editor_expanded, |this| this.h_full())
.child({
let settings = ThemeSettings::get_global(cx);
let font_size = TextSize::Small
.rems(cx)
.to_pixels(settings.agent_font_size(cx));
let line_height = settings.buffer_line_height.value() * font_size;
let text_style = TextStyle {
color: cx.theme().colors().text,
@ -1329,15 +1327,14 @@ impl Render for MessageEditor {
let action_log = self.thread.read(cx).action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
let font_size = TextSize::Small.rems(cx);
let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
v_flex()
.size_full()
.when(changed_buffers.len() > 0, |parent| {
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
})
.child(self.render_editor(font_size, line_height, window, cx))
.child(self.render_editor(window, cx))
.children({
let usage_callout = self.render_usage_callout(line_height, cx);

View File

@ -540,7 +540,6 @@ impl ToolCard for EditFileToolCard {
});
let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
let line_height = editor
.style()
.map(|style| style.text.line_height_in_pixels(window.rem_size()))
@ -558,7 +557,10 @@ impl ToolCard for EditFileToolCard {
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_size: ui_font_size.into(),
font_size: TextSize::Small
.rems(cx)
.to_pixels(settings.agent_font_size(cx))
.into(),
font_weight: settings.buffer_font.weight,
line_height: relative(settings.buffer_line_height.value()),
..Default::default()

View File

@ -106,6 +106,8 @@ pub struct ThemeSettings {
///
/// The terminal font family can be overridden using it's own setting.
pub buffer_font: Font,
/// The agent font size. Determines the size of text in the agent panel.
agent_font_size: Pixels,
/// The line height for buffers, and the terminal.
///
/// Changing this may affect the spacing of some UI elements.
@ -251,6 +253,11 @@ pub(crate) struct UiFontSize(Pixels);
impl Global for UiFontSize {}
#[derive(Default)]
pub(crate) struct AgentFontSize(Pixels);
impl Global for AgentFontSize {}
/// Represents the selection of a theme, which can be either static or dynamic.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(untagged)]
@ -409,6 +416,9 @@ pub struct ThemeSettingsContent {
#[serde(default)]
#[schemars(default = "default_font_features")]
pub buffer_font_features: Option<FontFeatures>,
/// The font size for the agent panel.
#[serde(default)]
pub agent_font_size: Option<f32>,
/// The name of the Zed theme to use.
#[serde(default)]
pub theme: Option<ThemeSelection>,
@ -579,6 +589,15 @@ impl ThemeSettings {
clamp_font_size(font_size)
}
/// Returns the UI font size.
pub fn agent_font_size(&self, cx: &App) -> Pixels {
let font_size = cx
.try_global::<AgentFontSize>()
.map(|size| size.0)
.unwrap_or(self.agent_font_size);
clamp_font_size(font_size)
}
/// Returns the buffer font size, read from the settings.
///
/// The real buffer font size is stored in-memory, to support temporary font size changes.
@ -746,6 +765,26 @@ pub fn reset_ui_font_size(cx: &mut App) {
}
}
/// Sets the adjusted UI font size.
pub fn adjust_agent_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) {
let agent_font_size = ThemeSettings::get_global(cx).agent_font_size(cx);
let mut adjusted_size = cx
.try_global::<AgentFontSize>()
.map_or(agent_font_size, |adjusted_size| adjusted_size.0);
f(&mut adjusted_size);
cx.set_global(AgentFontSize(clamp_font_size(adjusted_size)));
cx.refresh_windows();
}
/// Resets the UI font size to the default value.
pub fn reset_agent_font_size(cx: &mut App) {
if cx.has_global::<AgentFontSize>() {
cx.remove_global::<AgentFontSize>();
cx.refresh_windows();
}
}
/// Ensures font size is within the valid range.
pub fn clamp_font_size(size: Pixels) -> Pixels {
size.max(MIN_FONT_SIZE)
@ -789,6 +828,7 @@ impl settings::Settings for ThemeSettings {
},
buffer_font_size: defaults.buffer_font_size.unwrap().into(),
buffer_line_height: defaults.buffer_line_height.unwrap(),
agent_font_size: defaults.agent_font_size.unwrap().into(),
theme_selection: defaults.theme.clone(),
active_theme: themes
.get(defaults.theme.as_ref().unwrap().theme(*system_appearance))
@ -891,6 +931,12 @@ impl settings::Settings for ThemeSettings {
);
this.buffer_font_size = this.buffer_font_size.clamp(px(6.), px(100.));
merge(
&mut this.agent_font_size,
value.agent_font_size.map(Into::into),
);
this.agent_font_size = this.agent_font_size.clamp(px(6.), px(100.));
merge(&mut this.buffer_line_height, value.buffer_line_height);
// Clamp the `unnecessary_code_fade` to ensure text can't disappear entirely.