diff --git a/assets/settings/default.json b/assets/settings/default.json index 72d1100ef0..29dae37786 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.