From 31419db05fa55a37785455b56b6cdd47d8aaea39 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 5 May 2025 16:13:14 -0600 Subject: [PATCH] Allow the agent panel font size to be customized (#29954) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- assets/settings/default.json | 2 + crates/agent/src/active_thread.rs | 375 ++++++++++--------- crates/agent/src/assistant_panel.rs | 53 +++ crates/agent/src/message_editor.rs | 17 +- crates/assistant_tools/src/edit_file_tool.rs | 6 +- crates/theme/src/settings.rs | 46 +++ 6 files changed, 310 insertions(+), 189 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 6b8d050cb9..21b6e8f975 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -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. diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index 52d329bfdc..2320c305ed 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -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::(|_, 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, ) -> 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() } diff --git a/crates/agent/src/assistant_panel.rs b/crates/agent/src/assistant_panel.rs index b3b7549afd..bf9fe04b88 100644 --- a/crates/agent/src/assistant_panel.rs +++ b/crates/agent/src/assistant_panel.rs @@ -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.adjust_font_size(action.persist, px(1.0), cx); + } + + pub fn decrease_font_size( + &mut self, + action: &DecreaseBufferFontSize, + _: &mut Window, + cx: &mut Context, + ) { + self.adjust_font_size(action.persist, px(-1.0), cx); + } + + fn adjust_font_size(&mut self, persist: bool, delta: Pixels, cx: &mut Context) { + if persist { + update_settings_file::(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, + ) { + if action.persist { + update_settings_file::(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 diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs index b6af855217..007dd5b11e 100644 --- a/crates/agent/src/message_editor.rs +++ b/crates/agent/src/message_editor.rs @@ -509,13 +509,7 @@ impl MessageEditor { })) } - fn render_editor( - &self, - font_size: Rems, - line_height: Pixels, - window: &mut Window, - cx: &mut Context, - ) -> Div { + fn render_editor(&self, window: &mut Window, cx: &mut Context) -> 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); diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 814f2948ce..648a6fce65 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -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() diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 837abaee60..12f23ee6bd 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -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, + /// The font size for the agent panel. + #[serde(default)] + pub agent_font_size: Option, /// The name of the Zed theme to use. #[serde(default)] pub theme: Option, @@ -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::() + .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::() + .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::() { + cx.remove_global::(); + 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.