From 4896e0bc02139d67c0c4b036f9005f2cd56d4f9b 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 | 367 ++++++++++--------- 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, 306 insertions(+), 185 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 72d1100ef0955a7c4530eae7bf295a8fff705fd6..29dae37786e768e7f0ede17ec0beaf6e47db7227 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 52d329bfdc3394693e4e1ddd511417f73cace1bc..2320c305ed1915ad793f88d654ea6acc210223fe 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) + 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), + ) + .into_any_element() + } else if let Some(error) = error { + restore_checkpoint_button + .tooltip(Tooltip::text(error.to_string())) + .into_any_element() } 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), - ) - .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() - }; + 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() + 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( - Button::new("dismiss-feedback-message", "Cancel") - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &menu::Cancel, - &focus_handle, - window, - cx, + h_flex() + .gap_1() + .justify_end() + .child( + Button::new( + "dismiss-feedback-message", + "Cancel", ) - .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, + .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(); + }, + )), ) - .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() - }), - ), + .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), - ) - }) + }) + .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 b3b7549afdf31dcf2a467dc0e2103e054571aed9..bf9fe04b8858be7eb44cb691e5b3e5bfb2fe9f88 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 b6af8552176463472d297e38db98397f91a0e369..007dd5b11eb3a7092d34cfdaa36cb47c5788e6de 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 814f2948ce111d7ca3e6e41a7039db0336a82424..648a6fce65257158be9a4bf41789db34665e2a10 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 837abaee600ea0a8f5455186cedaf41a8572d76c..12f23ee6bdfc1adeba14e0eacbfa27982e0dea1b 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.