diff --git a/Cargo.lock b/Cargo.lock index b4088336611d0434ce7938f8216cdd30c1aec709..9a62a28284602da3f71e20294536376e6f40c235 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -330,6 +330,7 @@ dependencies = [ "anyhow", "cloud_llm_client", "collections", + "convert_case 0.8.0", "fs", "gpui", "language_model", @@ -339,6 +340,7 @@ dependencies = [ "serde_json", "serde_json_lenient", "settings", + "util", "workspace-hack", ] @@ -383,7 +385,6 @@ dependencies = [ "html_to_markdown", "http_client", "indoc", - "inventory", "itertools 0.14.0", "jsonschema", "language", @@ -434,7 +435,6 @@ dependencies = [ "url", "urlencoding", "util", - "uuid", "watch", "workspace", "workspace-hack", @@ -20505,7 +20505,6 @@ dependencies = [ "ashpd", "askpass", "assets", - "assistant_tool", "assistant_tools", "audio", "auto_update", @@ -20550,7 +20549,6 @@ dependencies = [ "gpui_tokio", "http_client", "image_viewer", - "indoc", "inspector_ui", "install_cli", "itertools 0.14.0", diff --git a/crates/agent/src/history_store.rs b/crates/agent/src/history_store.rs index 8f4c1a1e2e6533b4760956c60bfc1a26123df92a..285ce7c04b7268aea24e0a746737f65d7c3608df 100644 --- a/crates/agent/src/history_store.rs +++ b/crates/agent/src/history_store.rs @@ -1,7 +1,4 @@ -use crate::{ - ThreadId, - thread_store::{SerializedThreadMetadata, ThreadStore}, -}; +use crate::{ThreadId, thread_store::SerializedThreadMetadata}; use anyhow::{Context as _, Result}; use assistant_context::SavedContextMetadata; use chrono::{DateTime, Utc}; @@ -61,7 +58,6 @@ enum SerializedRecentOpen { } pub struct HistoryStore { - thread_store: Entity, context_store: Entity, recently_opened_entries: VecDeque, _subscriptions: Vec, @@ -70,15 +66,11 @@ pub struct HistoryStore { impl HistoryStore { pub fn new( - thread_store: Entity, context_store: Entity, initial_recent_entries: impl IntoIterator, cx: &mut Context, ) -> Self { - let subscriptions = vec![ - cx.observe(&thread_store, |_, _, cx| cx.notify()), - cx.observe(&context_store, |_, _, cx| cx.notify()), - ]; + let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())]; cx.spawn(async move |this, cx| { let entries = Self::load_recently_opened_entries(cx).await.log_err()?; @@ -96,7 +88,6 @@ impl HistoryStore { .detach(); Self { - thread_store, context_store, recently_opened_entries: initial_recent_entries.into_iter().collect(), _subscriptions: subscriptions, @@ -112,13 +103,6 @@ impl HistoryStore { return history_entries; } - history_entries.extend( - self.thread_store - .read(cx) - .reverse_chronological_threads() - .cloned() - .map(HistoryEntry::Thread), - ); history_entries.extend( self.context_store .read(cx) @@ -141,22 +125,6 @@ impl HistoryStore { return Vec::new(); } - let thread_entries = self - .thread_store - .read(cx) - .reverse_chronological_threads() - .flat_map(|thread| { - self.recently_opened_entries - .iter() - .enumerate() - .flat_map(|(index, entry)| match entry { - HistoryEntryId::Thread(id) if &thread.id == id => { - Some((index, HistoryEntry::Thread(thread.clone()))) - } - _ => None, - }) - }); - let context_entries = self.context_store .read(cx) @@ -173,8 +141,7 @@ impl HistoryStore { }) }); - thread_entries - .chain(context_entries) + context_entries // optimization to halt iteration early .take(self.recently_opened_entries.len()) .sorted_unstable_by_key(|(index, _)| *index) diff --git a/crates/agent_settings/Cargo.toml b/crates/agent_settings/Cargo.toml index d34396a5d35dd8919e519e804a93b50dfe046133..8af76053c2aabead30413c98e482ed97dbdbc361 100644 --- a/crates/agent_settings/Cargo.toml +++ b/crates/agent_settings/Cargo.toml @@ -15,11 +15,14 @@ path = "src/agent_settings.rs" anyhow.workspace = true cloud_llm_client.workspace = true collections.workspace = true +convert_case.workspace = true +fs.workspace = true gpui.workspace = true language_model.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true +util.workspace = true workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/agent_settings/src/agent_profile.rs b/crates/agent_settings/src/agent_profile.rs index 04fdd4a753a3fd015f2710fb9d70770ad960c560..42a273e2dcfccfb839e6e7d97efb42dcd7b0bba9 100644 --- a/crates/agent_settings/src/agent_profile.rs +++ b/crates/agent_settings/src/agent_profile.rs @@ -1,9 +1,15 @@ use std::sync::Arc; use collections::IndexMap; -use gpui::SharedString; +use convert_case::{Case, Casing as _}; +use fs::Fs; +use gpui::{App, SharedString}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use settings::{Settings as _, update_settings_file}; +use util::ResultExt as _; + +use crate::AgentSettings; pub mod builtin_profiles { use super::AgentProfileId; @@ -38,6 +44,69 @@ impl Default for AgentProfileId { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AgentProfile { + id: AgentProfileId, +} + +pub type AvailableProfiles = IndexMap; + +impl AgentProfile { + pub fn new(id: AgentProfileId) -> Self { + Self { id } + } + + pub fn id(&self) -> &AgentProfileId { + &self.id + } + + /// Saves a new profile to the settings. + pub fn create( + name: String, + base_profile_id: Option, + fs: Arc, + cx: &App, + ) -> AgentProfileId { + let id = AgentProfileId(name.to_case(Case::Kebab).into()); + + let base_profile = + base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned()); + + let profile_settings = AgentProfileSettings { + name: name.into(), + tools: base_profile + .as_ref() + .map(|profile| profile.tools.clone()) + .unwrap_or_default(), + enable_all_context_servers: base_profile + .as_ref() + .map(|profile| profile.enable_all_context_servers) + .unwrap_or_default(), + context_servers: base_profile + .map(|profile| profile.context_servers) + .unwrap_or_default(), + }; + + update_settings_file::(fs, cx, { + let id = id.clone(); + move |settings, _cx| { + settings.create_profile(id, profile_settings).log_err(); + } + }); + + id + } + + /// Returns a map of AgentProfileIds to their names + pub fn available_profiles(cx: &App) -> AvailableProfiles { + let mut profiles = AvailableProfiles::default(); + for (id, profile) in AgentSettings::get_global(cx).profiles.iter() { + profiles.insert(id.clone(), profile.name.clone()); + } + profiles + } +} + /// A profile for the Zed Agent that controls its behavior. #[derive(Debug, Clone)] pub struct AgentProfileSettings { diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index eaa058467f44638db4f0a446444424d706f76608..028db95c10a8c7a319bb05927dcabd0564a14683 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -52,7 +52,6 @@ gpui.workspace = true html_to_markdown.workspace = true http_client.workspace = true indoc.workspace = true -inventory.workspace = true itertools.workspace = true jsonschema.workspace = true language.workspace = true @@ -98,7 +97,6 @@ ui_input.workspace = true url.workspace = true urlencoding.workspace = true util.workspace = true -uuid.workspace = true watch.workspace = true workspace-hack.workspace = true workspace.workspace = true diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c205f24cd3aff16737b6b66f44d36b1e2e223fdc..bae2e107eeed39d978edbddada885b55fbf0414c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -62,9 +62,9 @@ use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; use crate::profile_selector::{ProfileProvider, ProfileSelector}; -use crate::ui::preview::UsageCallout; use crate::ui::{ AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip, + UsageCallout, }; use crate::{ AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode, diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs deleted file mode 100644 index 6dfadb691f3f9f7200991079da8e18bf11ce8853..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/active_thread.rs +++ /dev/null @@ -1,4110 +0,0 @@ -use crate::context_picker::{ContextPicker, MentionLink}; -use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; -use crate::message_editor::{extract_message_creases, insert_message_creases}; -use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill}; -use crate::{AgentPanel, ModelUsageContext}; -use agent::{ - ContextStore, LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, TextThreadStore, - Thread, ThreadError, ThreadEvent, ThreadFeedback, ThreadStore, ThreadSummary, - context::{self, AgentContextHandle, RULES_ICON}, - thread_store::RulesLoadingError, - tool_use::{PendingToolUseStatus, ToolUse}, -}; -use agent_settings::{AgentSettings, NotifyWhenAgentWaiting}; -use anyhow::Context as _; -use assistant_tool::ToolUseStatus; -use audio::{Audio, Sound}; -use cloud_llm_client::CompletionIntent; -use collections::{HashMap, HashSet}; -use editor::actions::{MoveUp, Paste}; -use editor::scroll::Autoscroll; -use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, SelectionEffects}; -use gpui::{ - AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry, - ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla, - ListAlignment, ListOffset, ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful, - StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, - WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, pulsating_between, -}; -use language::{Buffer, Language, LanguageRegistry}; -use language_model::{ - LanguageModelRequestMessage, LanguageModelToolUseId, MessageContent, Role, StopReason, -}; -use markdown::parser::{CodeBlockKind, CodeBlockMetadata}; -use markdown::{ - HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, PathWithRange, -}; -use project::{ProjectEntryId, ProjectItem as _}; -use rope::Point; -use settings::{Settings as _, SettingsStore, update_settings_file}; -use std::ffi::OsStr; -use std::path::Path; -use std::rc::Rc; -use std::sync::Arc; -use std::time::Duration; -use text::ToPoint; -use theme::ThemeSettings; -use ui::{ - Banner, CommonAnimationExt, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, - ScrollbarState, TextSize, Tooltip, prelude::*, -}; -use util::ResultExt as _; -use util::markdown::MarkdownCodeBlock; -use workspace::{CollaboratorId, Workspace}; -use zed_actions::assistant::OpenRulesLibrary; - -const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container"; -const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1; -const RESPONSE_PADDING_X: Pixels = px(19.); - -pub struct ActiveThread { - context_store: Entity, - language_registry: Arc, - thread_store: Entity, - text_thread_store: Entity, - thread: Entity, - workspace: WeakEntity, - save_thread_task: Option>, - messages: Vec, - list_state: ListState, - scrollbar_state: ScrollbarState, - rendered_messages_by_id: HashMap, - rendered_tool_uses: HashMap, - editing_message: Option<(MessageId, EditingMessageState)>, - expanded_tool_uses: HashMap, - expanded_thinking_segments: HashMap<(MessageId, usize), bool>, - expanded_code_blocks: HashMap<(MessageId, usize), bool>, - last_error: Option, - notifications: Vec>, - copied_code_block_ids: HashSet<(MessageId, usize)>, - _subscriptions: Vec, - notification_subscriptions: HashMap, Vec>, - open_feedback_editors: HashMap>, - _load_edited_message_context_task: Option>, -} - -struct RenderedMessage { - language_registry: Arc, - segments: Vec, -} - -#[derive(Clone)] -struct RenderedToolUse { - label: Entity, - input: Entity, - output: Entity, -} - -impl RenderedMessage { - fn from_segments( - segments: &[MessageSegment], - language_registry: Arc, - cx: &mut App, - ) -> Self { - let mut this = Self { - language_registry, - segments: Vec::with_capacity(segments.len()), - }; - for segment in segments { - this.push_segment(segment, cx); - } - this - } - - fn append_thinking(&mut self, text: &String, cx: &mut App) { - if let Some(RenderedMessageSegment::Thinking { - content, - scroll_handle, - }) = self.segments.last_mut() - { - content.update(cx, |markdown, cx| { - markdown.append(text, cx); - }); - scroll_handle.scroll_to_bottom(); - } else { - self.segments.push(RenderedMessageSegment::Thinking { - content: parse_markdown(text.into(), self.language_registry.clone(), cx), - scroll_handle: ScrollHandle::default(), - }); - } - } - - fn append_text(&mut self, text: &String, cx: &mut App) { - if let Some(RenderedMessageSegment::Text(markdown)) = self.segments.last_mut() { - markdown.update(cx, |markdown, cx| markdown.append(text, cx)); - } else { - self.segments - .push(RenderedMessageSegment::Text(parse_markdown( - SharedString::from(text), - self.language_registry.clone(), - cx, - ))); - } - } - - fn push_segment(&mut self, segment: &MessageSegment, cx: &mut App) { - match segment { - MessageSegment::Thinking { text, .. } => { - self.segments.push(RenderedMessageSegment::Thinking { - content: parse_markdown(text.into(), self.language_registry.clone(), cx), - scroll_handle: ScrollHandle::default(), - }) - } - MessageSegment::Text(text) => { - self.segments - .push(RenderedMessageSegment::Text(parse_markdown( - text.into(), - self.language_registry.clone(), - cx, - ))) - } - MessageSegment::RedactedThinking(_) => {} - }; - } -} - -enum RenderedMessageSegment { - Thinking { - content: Entity, - scroll_handle: ScrollHandle, - }, - Text(Entity), -} - -fn parse_markdown( - text: SharedString, - language_registry: Arc, - cx: &mut App, -) -> Entity { - cx.new(|cx| Markdown::new(text, Some(language_registry), None, cx)) -} - -pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { - let theme_settings = ThemeSettings::get_global(cx); - let colors = cx.theme().colors(); - let ui_font_size = TextSize::Default.rems(cx); - let buffer_font_size = TextSize::Small.rems(cx); - let mut text_style = window.text_style(); - let line_height = buffer_font_size * 1.75; - - text_style.refine(&TextStyleRefinement { - font_family: Some(theme_settings.ui_font.family.clone()), - font_fallbacks: theme_settings.ui_font.fallbacks.clone(), - font_features: Some(theme_settings.ui_font.features.clone()), - font_size: Some(ui_font_size.into()), - line_height: Some(line_height.into()), - color: Some(cx.theme().colors().text), - ..Default::default() - }); - - MarkdownStyle { - base_text_style: text_style.clone(), - syntax: cx.theme().syntax().clone(), - selection_background_color: cx.theme().colors().element_selection_background, - code_block_overflow_x_scroll: true, - table_overflow_x_scroll: true, - heading_level_styles: Some(HeadingLevelStyles { - h1: Some(TextStyleRefinement { - font_size: Some(rems(1.15).into()), - ..Default::default() - }), - h2: Some(TextStyleRefinement { - font_size: Some(rems(1.1).into()), - ..Default::default() - }), - h3: Some(TextStyleRefinement { - font_size: Some(rems(1.05).into()), - ..Default::default() - }), - h4: Some(TextStyleRefinement { - font_size: Some(rems(1.).into()), - ..Default::default() - }), - h5: Some(TextStyleRefinement { - font_size: Some(rems(0.95).into()), - ..Default::default() - }), - h6: Some(TextStyleRefinement { - font_size: Some(rems(0.875).into()), - ..Default::default() - }), - }), - code_block: StyleRefinement { - padding: EdgesRefinement { - top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))), - left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))), - right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))), - bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))), - }, - background: Some(colors.editor_background.into()), - text: Some(TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), - font_features: Some(theme_settings.buffer_font.features.clone()), - font_size: Some(buffer_font_size.into()), - ..Default::default() - }), - ..Default::default() - }, - inline_code: TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), - font_features: Some(theme_settings.buffer_font.features.clone()), - font_size: Some(buffer_font_size.into()), - background_color: Some(colors.editor_foreground.opacity(0.08)), - ..Default::default() - }, - link: TextStyleRefinement { - background_color: Some(colors.editor_foreground.opacity(0.025)), - underline: Some(UnderlineStyle { - color: Some(colors.text_accent.opacity(0.5)), - thickness: px(1.), - ..Default::default() - }), - ..Default::default() - }, - link_callback: Some(Rc::new(move |url, cx| { - if MentionLink::is_valid(url) { - let colors = cx.theme().colors(); - Some(TextStyleRefinement { - background_color: Some(colors.element_background), - ..Default::default() - }) - } else { - None - } - })), - ..Default::default() - } -} - -fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle { - let theme_settings = ThemeSettings::get_global(cx); - let colors = cx.theme().colors(); - let ui_font_size = TextSize::Default.rems(cx); - let buffer_font_size = TextSize::Small.rems(cx); - let mut text_style = window.text_style(); - - text_style.refine(&TextStyleRefinement { - font_family: Some(theme_settings.ui_font.family.clone()), - font_fallbacks: theme_settings.ui_font.fallbacks.clone(), - font_features: Some(theme_settings.ui_font.features.clone()), - font_size: Some(ui_font_size.into()), - color: Some(cx.theme().colors().text), - ..Default::default() - }); - - MarkdownStyle { - base_text_style: text_style, - syntax: cx.theme().syntax().clone(), - selection_background_color: cx.theme().colors().element_selection_background, - code_block_overflow_x_scroll: false, - code_block: StyleRefinement { - margin: EdgesRefinement::default(), - padding: EdgesRefinement::default(), - background: Some(colors.editor_background.into()), - border_color: None, - border_widths: EdgesRefinement::default(), - text: Some(TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), - font_features: Some(theme_settings.buffer_font.features.clone()), - font_size: Some(buffer_font_size.into()), - ..Default::default() - }), - ..Default::default() - }, - inline_code: TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), - font_features: Some(theme_settings.buffer_font.features.clone()), - font_size: Some(TextSize::XSmall.rems(cx).into()), - ..Default::default() - }, - heading: StyleRefinement { - text: Some(TextStyleRefinement { - font_size: Some(ui_font_size.into()), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() - } -} - -fn render_markdown_code_block( - message_id: MessageId, - ix: usize, - kind: &CodeBlockKind, - parsed_markdown: &ParsedMarkdown, - metadata: CodeBlockMetadata, - active_thread: Entity, - workspace: WeakEntity, - _window: &Window, - cx: &App, -) -> Div { - let label_size = rems(0.8125); - - let label = match kind { - CodeBlockKind::Indented => None, - CodeBlockKind::Fenced => Some( - h_flex() - .px_1() - .gap_1() - .child( - Icon::new(IconName::Code) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - .child(div().text_size(label_size).child("Plain Text")) - .into_any_element(), - ), - CodeBlockKind::FencedLang(raw_language_name) => Some(render_code_language( - parsed_markdown.languages_by_name.get(raw_language_name), - raw_language_name.clone(), - cx, - )), - CodeBlockKind::FencedSrc(path_range) => path_range.path.file_name().map(|file_name| { - // We tell the model to use /dev/null for the path instead of using ```language - // because otherwise it consistently fails to use code citations. - if path_range.path.starts_with("/dev/null") { - let ext = path_range - .path - .extension() - .and_then(OsStr::to_str) - .map(|str| SharedString::new(str.to_string())) - .unwrap_or_default(); - - render_code_language( - parsed_markdown - .languages_by_path - .get(&path_range.path) - .or_else(|| parsed_markdown.languages_by_name.get(&ext)), - ext, - cx, - ) - } else { - let content = if let Some(parent) = path_range.path.parent() { - let file_name = file_name.to_string_lossy().to_string(); - let path = parent.to_string_lossy().to_string(); - let path_and_file = format!("{}/{}", path, file_name); - - h_flex() - .id(("code-block-header-label", ix)) - .ml_1() - .gap_1() - .child(div().text_size(label_size).child(file_name)) - .child(Label::new(path).color(Color::Muted).size(LabelSize::Small)) - .tooltip(move |window, cx| { - Tooltip::with_meta( - "Jump to File", - None, - path_and_file.clone(), - window, - cx, - ) - }) - .into_any_element() - } else { - div() - .ml_1() - .text_size(label_size) - .child(path_range.path.to_string_lossy().to_string()) - .into_any_element() - }; - - h_flex() - .id(("code-block-header-button", ix)) - .w_full() - .max_w_full() - .px_1() - .gap_0p5() - .cursor_pointer() - .rounded_sm() - .hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5))) - .child( - h_flex() - .gap_0p5() - .children( - file_icons::FileIcons::get_icon(&path_range.path, cx) - .map(Icon::from_path) - .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)), - ) - .child(content) - .child( - Icon::new(IconName::ArrowUpRight) - .size(IconSize::Small) - .color(Color::Ignored), - ), - ) - .on_click({ - let path_range = path_range.clone(); - move |_, window, cx| { - workspace - .update(cx, |workspace, cx| { - open_path(&path_range, window, workspace, cx) - }) - .ok(); - } - }) - .into_any_element() - } - }), - }; - - let codeblock_was_copied = active_thread - .read(cx) - .copied_code_block_ids - .contains(&(message_id, ix)); - - let is_expanded = active_thread.read(cx).is_codeblock_expanded(message_id, ix); - - let codeblock_header_bg = cx - .theme() - .colors() - .element_background - .blend(cx.theme().colors().editor_foreground.opacity(0.025)); - - let control_buttons = h_flex() - .visible_on_hover(CODEBLOCK_CONTAINER_GROUP) - .absolute() - .top_0() - .right_0() - .h_full() - .bg(codeblock_header_bg) - .rounded_tr_md() - .px_1() - .gap_1() - .child( - IconButton::new( - ("copy-markdown-code", ix), - if codeblock_was_copied { - IconName::Check - } else { - IconName::Copy - }, - ) - .icon_color(Color::Muted) - .shape(ui::IconButtonShape::Square) - .tooltip(Tooltip::text("Copy Code")) - .on_click({ - let active_thread = active_thread.clone(); - let parsed_markdown = parsed_markdown.clone(); - let code_block_range = metadata.content_range; - move |_event, _window, cx| { - active_thread.update(cx, |this, cx| { - this.copied_code_block_ids.insert((message_id, ix)); - - let code = parsed_markdown.source()[code_block_range.clone()].to_string(); - cx.write_to_clipboard(ClipboardItem::new_string(code)); - - cx.spawn(async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(2)).await; - - cx.update(|cx| { - this.update(cx, |this, cx| { - this.copied_code_block_ids.remove(&(message_id, ix)); - cx.notify(); - }) - }) - .ok(); - }) - .detach(); - }); - } - }), - ) - .child( - IconButton::new( - ("expand-collapse-code", ix), - if is_expanded { - IconName::ChevronUp - } else { - IconName::ChevronDown - }, - ) - .icon_color(Color::Muted) - .shape(ui::IconButtonShape::Square) - .tooltip(Tooltip::text(if is_expanded { - "Collapse Code" - } else { - "Expand Code" - })) - .on_click({ - move |_event, _window, cx| { - active_thread.update(cx, |this, cx| { - this.toggle_codeblock_expanded(message_id, ix); - cx.notify(); - }); - } - }), - ); - - let codeblock_header = h_flex() - .relative() - .p_1() - .gap_1() - .justify_between() - .bg(codeblock_header_bg) - .map(|this| { - if !is_expanded { - this.rounded_md() - } else { - this.rounded_t_md() - .border_b_1() - .border_color(cx.theme().colors().border.opacity(0.6)) - } - }) - .children(label) - .child(control_buttons); - - v_flex() - .group(CODEBLOCK_CONTAINER_GROUP) - .my_2() - .overflow_hidden() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border.opacity(0.6)) - .bg(cx.theme().colors().editor_background) - .child(codeblock_header) - .when(!is_expanded, |this| this.h(rems_from_px(31.))) -} - -fn open_path( - path_range: &PathWithRange, - window: &mut Window, - workspace: &mut Workspace, - cx: &mut Context<'_, Workspace>, -) { - let Some(project_path) = workspace - .project() - .read(cx) - .find_project_path(&path_range.path, cx) - else { - return; // TODO instead of just bailing out, open that path in a buffer. - }; - - let Some(target) = path_range.range.as_ref().map(|range| { - Point::new( - // Line number is 1-based - range.start.line.saturating_sub(1), - range.start.col.unwrap_or(0), - ) - }) else { - return; - }; - let open_task = workspace.open_path(project_path, None, true, window, cx); - window - .spawn(cx, async move |cx| { - let item = open_task.await?; - if let Some(active_editor) = item.downcast::() { - active_editor - .update_in(cx, |editor, window, cx| { - editor.go_to_singleton_buffer_point(target, window, cx); - }) - .ok(); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); -} - -fn render_code_language( - language: Option<&Arc>, - name_fallback: SharedString, - cx: &App, -) -> AnyElement { - let icon_path = language.and_then(|language| { - language - .config() - .matcher - .path_suffixes - .iter() - .find_map(|extension| file_icons::FileIcons::get_icon(Path::new(extension), cx)) - .map(Icon::from_path) - }); - - let language_label = language - .map(|language| language.name().into()) - .unwrap_or(name_fallback); - - let label_size = rems(0.8125); - - h_flex() - .px_1() - .gap_1p5() - .children(icon_path.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall))) - .child(div().text_size(label_size).child(language_label)) - .into_any_element() -} - -fn open_markdown_link( - text: SharedString, - workspace: WeakEntity, - window: &mut Window, - cx: &mut App, -) { - let Some(workspace) = workspace.upgrade() else { - cx.open_url(&text); - return; - }; - - match MentionLink::try_parse(&text, &workspace, cx) { - Some(MentionLink::File(path, entry)) => workspace.update(cx, |workspace, cx| { - if entry.is_dir() { - workspace.project().update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(entry.id)); - }) - } else { - workspace - .open_path(path, None, true, window, cx) - .detach_and_log_err(cx); - } - }), - Some(MentionLink::Symbol(path, symbol_name)) => { - let open_task = workspace.update(cx, |workspace, cx| { - workspace.open_path(path, None, true, window, cx) - }); - window - .spawn(cx, async move |cx| { - let active_editor = open_task - .await? - .downcast::() - .context("Item is not an editor")?; - active_editor.update_in(cx, |editor, window, cx| { - let symbol_range = editor - .buffer() - .read(cx) - .snapshot(cx) - .outline(None) - .and_then(|outline| { - outline - .find_most_similar(&symbol_name) - .map(|(_, item)| item.range.clone()) - }) - .context("Could not find matching symbol")?; - - editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |s| s.select_anchor_ranges([symbol_range.start..symbol_range.start]), - ); - anyhow::Ok(()) - }) - }) - .detach_and_log_err(cx); - } - Some(MentionLink::Selection(path, line_range)) => { - let open_task = workspace.update(cx, |workspace, cx| { - workspace.open_path(path, None, true, window, cx) - }); - window - .spawn(cx, async move |cx| { - let active_editor = open_task - .await? - .downcast::() - .context("Item is not an editor")?; - active_editor.update_in(cx, |editor, window, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |s| { - s.select_ranges([Point::new(line_range.start as u32, 0) - ..Point::new(line_range.start as u32, 0)]) - }, - ); - anyhow::Ok(()) - }) - }) - .detach_and_log_err(cx); - } - Some(MentionLink::Thread(thread_id)) => workspace.update(cx, |workspace, cx| { - if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| { - panel - .open_thread_by_id(&thread_id, window, cx) - .detach_and_log_err(cx) - }); - } - }), - Some(MentionLink::TextThread(path)) => workspace.update(cx, |workspace, cx| { - if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| { - panel - .open_saved_prompt_editor(path, window, cx) - .detach_and_log_err(cx); - }); - } - }), - Some(MentionLink::Fetch(url)) => cx.open_url(&url), - Some(MentionLink::Rule(prompt_id)) => window.dispatch_action( - Box::new(OpenRulesLibrary { - prompt_to_select: Some(prompt_id.0), - }), - cx, - ), - None => cx.open_url(&text), - } -} - -struct EditingMessageState { - editor: Entity, - context_strip: Entity, - context_picker_menu_handle: PopoverMenuHandle, - last_estimated_token_count: Option, - _subscriptions: [Subscription; 2], - _update_token_count_task: Option>, -} - -impl ActiveThread { - pub fn new( - thread: Entity, - thread_store: Entity, - text_thread_store: Entity, - context_store: Entity, - language_registry: Arc, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let subscriptions = vec![ - 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.)); - - let workspace_subscription = workspace.upgrade().map(|workspace| { - cx.observe_release(&workspace, |this, _, cx| { - this.dismiss_notifications(cx); - }) - }); - - let mut this = Self { - language_registry, - thread_store, - text_thread_store, - context_store, - thread: thread.clone(), - workspace, - save_thread_task: None, - messages: Vec::new(), - rendered_messages_by_id: HashMap::default(), - rendered_tool_uses: HashMap::default(), - expanded_tool_uses: HashMap::default(), - expanded_thinking_segments: HashMap::default(), - expanded_code_blocks: HashMap::default(), - list_state: list_state.clone(), - scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), - editing_message: None, - last_error: None, - copied_code_block_ids: HashSet::default(), - notifications: Vec::new(), - _subscriptions: subscriptions, - notification_subscriptions: HashMap::default(), - open_feedback_editors: HashMap::default(), - _load_edited_message_context_task: None, - }; - - for message in thread.read(cx).messages().cloned().collect::>() { - let rendered_message = RenderedMessage::from_segments( - &message.segments, - this.language_registry.clone(), - cx, - ); - this.push_rendered_message(message.id, rendered_message); - - for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) { - this.render_tool_use_markdown( - tool_use.id.clone(), - tool_use.ui_text.clone(), - &serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(), - tool_use.status.text(), - cx, - ); - } - } - - if let Some(subscription) = workspace_subscription { - this._subscriptions.push(subscription); - } - - this - } - - pub fn thread(&self) -> &Entity { - &self.thread - } - - pub fn is_empty(&self) -> bool { - self.messages.is_empty() - } - - pub fn summary<'a>(&'a self, cx: &'a App) -> &'a ThreadSummary { - self.thread.read(cx).summary() - } - - pub fn regenerate_summary(&self, cx: &mut App) { - self.thread.update(cx, |thread, cx| thread.summarize(cx)) - } - - pub fn cancel_last_completion(&mut self, window: &mut Window, cx: &mut App) -> bool { - self.last_error.take(); - self.thread.update(cx, |thread, cx| { - thread.cancel_last_completion(Some(window.window_handle()), cx) - }) - } - - pub fn last_error(&self) -> Option { - self.last_error.clone() - } - - pub fn clear_last_error(&mut self) { - self.last_error.take(); - } - - /// Returns the editing message id and the estimated token count in the content - pub fn editing_message_id(&self) -> Option<(MessageId, u64)> { - self.editing_message - .as_ref() - .map(|(id, state)| (*id, state.last_estimated_token_count.unwrap_or(0))) - } - - pub fn context_store(&self) -> &Entity { - &self.context_store - } - - pub fn thread_store(&self) -> &Entity { - &self.thread_store - } - - pub fn text_thread_store(&self) -> &Entity { - &self.text_thread_store - } - - fn push_rendered_message(&mut self, id: MessageId, rendered_message: RenderedMessage) { - let old_len = self.messages.len(); - self.messages.push(id); - self.list_state.splice(old_len..old_len, 1); - self.rendered_messages_by_id.insert(id, rendered_message); - } - - fn deleted_message(&mut self, id: &MessageId) { - let Some(index) = self.messages.iter().position(|message_id| message_id == id) else { - return; - }; - self.messages.remove(index); - self.list_state.splice(index..index + 1, 0); - self.rendered_messages_by_id.remove(id); - } - - fn render_tool_use_markdown( - &mut self, - tool_use_id: LanguageModelToolUseId, - tool_label: impl Into, - tool_input: &str, - tool_output: SharedString, - cx: &mut Context, - ) { - let rendered = self - .rendered_tool_uses - .entry(tool_use_id) - .or_insert_with(|| RenderedToolUse { - label: cx.new(|cx| { - Markdown::new("".into(), Some(self.language_registry.clone()), None, cx) - }), - input: cx.new(|cx| { - Markdown::new("".into(), Some(self.language_registry.clone()), None, cx) - }), - output: cx.new(|cx| { - Markdown::new("".into(), Some(self.language_registry.clone()), None, cx) - }), - }); - - rendered.label.update(cx, |this, cx| { - this.replace(tool_label, cx); - }); - rendered.input.update(cx, |this, cx| { - this.replace( - MarkdownCodeBlock { - tag: "json", - text: tool_input, - } - .to_string(), - cx, - ); - }); - rendered.output.update(cx, |this, cx| { - this.replace(tool_output, cx); - }); - } - - fn handle_thread_event( - &mut self, - _thread: &Entity, - event: &ThreadEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - ThreadEvent::CancelEditing => { - if self.editing_message.is_some() { - self.cancel_editing_message(&menu::Cancel, window, cx); - } - } - ThreadEvent::ShowError(error) => { - self.last_error = Some(error.clone()); - } - ThreadEvent::NewRequest => { - cx.notify(); - } - ThreadEvent::CompletionCanceled => { - self.thread.update(cx, |thread, cx| { - thread.project().update(cx, |project, cx| { - project.set_agent_location(None, cx); - }) - }); - self.workspace - .update(cx, |workspace, cx| { - if workspace.is_being_followed(CollaboratorId::Agent) { - workspace.unfollow(CollaboratorId::Agent, window, cx); - } - }) - .ok(); - cx.notify(); - } - ThreadEvent::StreamedCompletion - | ThreadEvent::SummaryGenerated - | ThreadEvent::SummaryChanged => { - self.save_thread(cx); - } - ThreadEvent::Stopped(reason) => { - match reason { - Ok(StopReason::EndTurn | StopReason::MaxTokens) => { - let used_tools = self.thread.read(cx).used_tools_since_last_user_message(); - self.notify_with_sound( - if used_tools { - "Finished running tools" - } else { - "New message" - }, - IconName::ZedAssistant, - window, - cx, - ); - } - Ok(StopReason::ToolUse) => { - // Don't notify for intermediate tool use - } - Ok(StopReason::Refusal) => { - let model_name = self - .thread - .read(cx) - .configured_model() - .map(|configured| configured.model.name().0.to_string()) - .unwrap_or_else(|| "The model".to_string()); - let refusal_message = format!( - "{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.", - model_name - ); - self.last_error = Some(ThreadError::Message { - header: SharedString::from("Request Refused"), - message: SharedString::from(refusal_message), - }); - self.notify_with_sound( - format!("{} refused to respond", model_name), - IconName::Warning, - window, - cx, - ); - } - Err(error) => { - self.notify_with_sound( - "Agent stopped due to an error", - IconName::Warning, - window, - cx, - ); - - let error_message = error - .chain() - .map(|err| err.to_string()) - .collect::>() - .join("\n"); - self.last_error = Some(ThreadError::Message { - header: "Error".into(), - message: error_message.into(), - }); - } - } - } - ThreadEvent::ToolConfirmationNeeded => { - self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx); - } - ThreadEvent::ToolUseLimitReached => { - self.notify_with_sound( - "Consecutive tool use limit reached.", - IconName::Warning, - window, - cx, - ); - } - ThreadEvent::StreamedAssistantText(message_id, text) => { - if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(message_id) { - rendered_message.append_text(text, cx); - } - } - ThreadEvent::StreamedAssistantThinking(message_id, text) => { - if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(message_id) { - rendered_message.append_thinking(text, cx); - } - } - ThreadEvent::MessageAdded(message_id) => { - self.clear_last_error(); - if let Some(rendered_message) = self.thread.update(cx, |thread, cx| { - thread.message(*message_id).map(|message| { - RenderedMessage::from_segments( - &message.segments, - self.language_registry.clone(), - cx, - ) - }) - }) { - self.push_rendered_message(*message_id, rendered_message); - } - - self.save_thread(cx); - cx.notify(); - } - ThreadEvent::MessageEdited(message_id) => { - self.clear_last_error(); - if let Some(index) = self.messages.iter().position(|id| id == message_id) - && let Some(rendered_message) = self.thread.update(cx, |thread, cx| { - thread.message(*message_id).map(|message| { - let mut rendered_message = RenderedMessage { - language_registry: self.language_registry.clone(), - segments: Vec::with_capacity(message.segments.len()), - }; - for segment in &message.segments { - rendered_message.push_segment(segment, cx); - } - rendered_message - }) - }) - { - self.list_state.splice(index..index + 1, 1); - self.rendered_messages_by_id - .insert(*message_id, rendered_message); - self.scroll_to_bottom(cx); - self.save_thread(cx); - cx.notify(); - } - } - ThreadEvent::MessageDeleted(message_id) => { - self.deleted_message(message_id); - self.save_thread(cx); - cx.notify(); - } - ThreadEvent::UsePendingTools { tool_uses } => { - for tool_use in tool_uses { - self.render_tool_use_markdown( - tool_use.id.clone(), - tool_use.ui_text.clone(), - &serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(), - "".into(), - cx, - ); - } - } - ThreadEvent::StreamedToolUse { - tool_use_id, - ui_text, - input, - } => { - self.render_tool_use_markdown( - tool_use_id.clone(), - ui_text.clone(), - &serde_json::to_string_pretty(&input).unwrap_or_default(), - "".into(), - cx, - ); - } - ThreadEvent::ToolFinished { - pending_tool_use, .. - } => { - if let Some(tool_use) = pending_tool_use { - self.render_tool_use_markdown( - tool_use.id.clone(), - tool_use.ui_text.clone(), - &serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(), - self.thread - .read(cx) - .output_for_tool(&tool_use.id) - .map(|output| output.clone().into()) - .unwrap_or("".into()), - cx, - ); - } - } - ThreadEvent::CheckpointChanged => cx.notify(), - ThreadEvent::ReceivedTextChunk => {} - ThreadEvent::InvalidToolInput { - tool_use_id, - ui_text, - invalid_input_json, - } => { - self.render_tool_use_markdown( - tool_use_id.clone(), - ui_text, - invalid_input_json, - self.thread - .read(cx) - .output_for_tool(tool_use_id) - .map(|output| output.clone().into()) - .unwrap_or("".into()), - cx, - ); - } - ThreadEvent::MissingToolUse { - tool_use_id, - ui_text, - } => { - self.render_tool_use_markdown( - tool_use_id.clone(), - ui_text, - "", - self.thread - .read(cx) - .output_for_tool(tool_use_id) - .map(|output| output.clone().into()) - .unwrap_or("".into()), - cx, - ); - } - ThreadEvent::ProfileChanged => { - self.save_thread(cx); - cx.notify(); - } - } - } - - fn handle_rules_loading_error( - &mut self, - _thread_store: Entity, - error: &RulesLoadingError, - cx: &mut Context, - ) { - self.last_error = Some(ThreadError::Message { - header: "Error loading rules file".into(), - message: error.message.clone(), - }); - cx.notify(); - } - - fn play_notification_sound(&self, window: &Window, cx: &mut App) { - let settings = AgentSettings::get_global(cx); - if settings.play_sound_when_agent_done && !window.is_window_active() { - Audio::play_sound(Sound::AgentDone, cx); - } - } - - fn show_notification( - &mut self, - caption: impl Into, - icon: IconName, - window: &mut Window, - cx: &mut Context, - ) { - if window.is_window_active() || !self.notifications.is_empty() { - return; - } - - let title = self.thread.read(cx).summary().unwrap_or("Agent Panel"); - - match AgentSettings::get_global(cx).notify_when_agent_waiting { - NotifyWhenAgentWaiting::PrimaryScreen => { - if let Some(primary) = cx.primary_display() { - self.pop_up(icon, caption.into(), title, window, primary, cx); - } - } - NotifyWhenAgentWaiting::AllScreens => { - let caption = caption.into(); - for screen in cx.displays() { - self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx); - } - } - NotifyWhenAgentWaiting::Never => { - // Don't show anything - } - } - } - - fn notify_with_sound( - &mut self, - caption: impl Into, - icon: IconName, - window: &mut Window, - cx: &mut Context, - ) { - self.play_notification_sound(window, cx); - self.show_notification(caption, icon, window, cx); - } - - fn pop_up( - &mut self, - icon: IconName, - caption: SharedString, - title: SharedString, - window: &mut Window, - screen: Rc, - cx: &mut Context<'_, ActiveThread>, - ) { - let options = AgentNotification::window_options(screen, cx); - - let project_name = self.workspace.upgrade().and_then(|workspace| { - workspace - .read(cx) - .project() - .read(cx) - .visible_worktrees(cx) - .next() - .map(|worktree| worktree.read(cx).root_name().to_string()) - }); - - if let Some(screen_window) = cx - .open_window(options, |_, cx| { - cx.new(|_| { - AgentNotification::new(title.clone(), caption.clone(), icon, project_name) - }) - }) - .log_err() - && let Some(pop_up) = screen_window.entity(cx).log_err() - { - self.notification_subscriptions - .entry(screen_window) - .or_insert_with(Vec::new) - .push(cx.subscribe_in(&pop_up, window, { - |this, _, event, window, cx| match event { - AgentNotificationEvent::Accepted => { - let handle = window.window_handle(); - cx.activate(true); - - let workspace_handle = this.workspace.clone(); - - // If there are multiple Zed windows, activate the correct one. - cx.defer(move |cx| { - handle - .update(cx, |_view, window, _cx| { - window.activate_window(); - - if let Some(workspace) = workspace_handle.upgrade() { - workspace.update(_cx, |workspace, cx| { - workspace.focus_panel::(window, cx); - }); - } - }) - .log_err(); - }); - - this.dismiss_notifications(cx); - } - AgentNotificationEvent::Dismissed => { - this.dismiss_notifications(cx); - } - } - })); - - self.notifications.push(screen_window); - - // If the user manually refocuses the original window, dismiss the popup. - self.notification_subscriptions - .entry(screen_window) - .or_insert_with(Vec::new) - .push({ - let pop_up_weak = pop_up.downgrade(); - - cx.observe_window_activation(window, move |_, window, cx| { - if window.is_window_active() - && let Some(pop_up) = pop_up_weak.upgrade() - { - pop_up.update(cx, |_, cx| { - cx.emit(AgentNotificationEvent::Dismissed); - }); - } - }) - }); - } - } - - /// Spawns a task to save the active thread. - /// - /// Only one task to save the thread will be in flight at a time. - fn save_thread(&mut self, cx: &mut Context) { - let thread = self.thread.clone(); - self.save_thread_task = Some(cx.spawn(async move |this, cx| { - let task = this - .update(cx, |this, cx| { - this.thread_store - .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx)) - }) - .ok(); - - if let Some(task) = task { - task.await.log_err(); - } - })); - } - - fn start_editing_message( - &mut self, - message_id: MessageId, - message_text: impl Into>, - message_creases: &[MessageCrease], - window: &mut Window, - cx: &mut Context, - ) { - let editor = crate::message_editor::create_editor( - self.workspace.clone(), - self.context_store.downgrade(), - self.thread_store.downgrade(), - self.text_thread_store.downgrade(), - EDIT_PREVIOUS_MESSAGE_MIN_LINES, - None, - window, - cx, - ); - editor.update(cx, |editor, cx| { - editor.set_text(message_text, window, cx); - insert_message_creases(editor, message_creases, &self.context_store, window, cx); - editor.focus_handle(cx).focus(window); - editor.move_to_end(&editor::actions::MoveToEnd, window, cx); - }); - let buffer_edited_subscription = - cx.subscribe(&editor, |this, _, event: &EditorEvent, cx| { - if event == &EditorEvent::BufferEdited { - this.update_editing_message_token_count(true, cx); - } - }); - - let context_picker_menu_handle = PopoverMenuHandle::default(); - let context_strip = cx.new(|cx| { - ContextStrip::new( - self.context_store.clone(), - self.workspace.clone(), - Some(self.thread_store.downgrade()), - Some(self.text_thread_store.downgrade()), - context_picker_menu_handle.clone(), - SuggestContextKind::File, - ModelUsageContext::Thread(self.thread.clone()), - window, - cx, - ) - }); - - let context_strip_subscription = - cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event); - - self.editing_message = Some(( - message_id, - EditingMessageState { - editor: editor.clone(), - context_strip, - context_picker_menu_handle, - last_estimated_token_count: None, - _subscriptions: [buffer_edited_subscription, context_strip_subscription], - _update_token_count_task: None, - }, - )); - self.update_editing_message_token_count(false, cx); - cx.notify(); - } - - fn handle_context_strip_event( - &mut self, - _context_strip: &Entity, - event: &ContextStripEvent, - window: &mut Window, - cx: &mut Context, - ) { - if let Some((_, state)) = self.editing_message.as_ref() { - match event { - ContextStripEvent::PickerDismissed - | ContextStripEvent::BlurredEmpty - | ContextStripEvent::BlurredDown => { - let editor_focus_handle = state.editor.focus_handle(cx); - window.focus(&editor_focus_handle); - } - ContextStripEvent::BlurredUp => {} - } - } - } - - fn update_editing_message_token_count(&mut self, debounce: bool, cx: &mut Context) { - let Some((message_id, state)) = self.editing_message.as_mut() else { - return; - }; - - cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged); - state._update_token_count_task.take(); - - let Some(configured_model) = self.thread.read(cx).configured_model() else { - state.last_estimated_token_count.take(); - return; - }; - - let editor = state.editor.clone(); - let thread = self.thread.clone(); - let message_id = *message_id; - - state._update_token_count_task = Some(cx.spawn(async move |this, cx| { - if debounce { - cx.background_executor() - .timer(Duration::from_millis(200)) - .await; - } - - let token_count = if let Some(task) = cx - .update(|cx| { - let Some(message) = thread.read(cx).message(message_id) else { - log::error!("Message that was being edited no longer exists"); - return None; - }; - let message_text = editor.read(cx).text(cx); - - if message_text.is_empty() && message.loaded_context.is_empty() { - return None; - } - - let mut request_message = LanguageModelRequestMessage { - role: language_model::Role::User, - content: Vec::new(), - cache: false, - }; - - message - .loaded_context - .add_to_request_message(&mut request_message); - - if !message_text.is_empty() { - request_message - .content - .push(MessageContent::Text(message_text)); - } - - let request = language_model::LanguageModelRequest { - thread_id: None, - prompt_id: None, - intent: None, - mode: None, - messages: vec![request_message], - tools: vec![], - tool_choice: None, - stop: vec![], - temperature: AgentSettings::temperature_for_model( - &configured_model.model, - cx, - ), - thinking_allowed: true, - }; - - Some(configured_model.model.count_tokens(request, cx)) - }) - .ok() - .flatten() - { - task.await.log_err() - } else { - Some(0) - }; - - if let Some(token_count) = token_count { - this.update(cx, |this, cx| { - let Some((_message_id, state)) = this.editing_message.as_mut() else { - return; - }; - - state.last_estimated_token_count = Some(token_count); - cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged); - }) - .ok(); - }; - })); - } - - fn toggle_context_picker( - &mut self, - _: &crate::ToggleContextPicker, - window: &mut Window, - cx: &mut Context, - ) { - if let Some((_, state)) = self.editing_message.as_mut() { - let handle = state.context_picker_menu_handle.clone(); - window.defer(cx, move |window, cx| { - handle.toggle(window, cx); - }); - } - } - - fn remove_all_context( - &mut self, - _: &crate::RemoveAllContext, - _window: &mut Window, - cx: &mut Context, - ) { - self.context_store.update(cx, |store, cx| store.clear(cx)); - cx.notify(); - } - - fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) { - if let Some((_, state)) = self.editing_message.as_mut() { - if state.context_picker_menu_handle.is_deployed() { - cx.propagate(); - } else { - state.context_strip.focus_handle(cx).focus(window); - } - } - } - - fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context) { - attach_pasted_images_as_context(&self.context_store, cx); - } - - fn cancel_editing_message( - &mut self, - _: &menu::Cancel, - window: &mut Window, - cx: &mut Context, - ) { - self.editing_message.take(); - cx.notify(); - - if let Some(workspace) = self.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = workspace.panel::(cx) { - panel.focus_handle(cx).focus(window); - } - }); - } - } - - fn confirm_editing_message( - &mut self, - _: &menu::Confirm, - window: &mut Window, - cx: &mut Context, - ) { - let Some((message_id, state)) = self.editing_message.take() else { - return; - }; - - let Some(model) = self - .thread - .update(cx, |thread, cx| thread.get_or_init_configured_model(cx)) - else { - return; - }; - - let edited_text = state.editor.read(cx).text(cx); - - let creases = state.editor.update(cx, extract_message_creases); - - let new_context = self - .context_store - .read(cx) - .new_context_for_thread(self.thread.read(cx), Some(message_id)); - - let project = self.thread.read(cx).project().clone(); - let prompt_store = self.thread_store.read(cx).prompt_store().clone(); - - let git_store = project.read(cx).git_store().clone(); - let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx)); - - let load_context_task = context::load_context(new_context, &project, &prompt_store, cx); - self._load_edited_message_context_task = - Some(cx.spawn_in(window, async move |this, cx| { - let (context, checkpoint) = - futures::future::join(load_context_task, checkpoint).await; - let _ = this - .update_in(cx, |this, window, cx| { - this.thread.update(cx, |thread, cx| { - thread.edit_message( - message_id, - Role::User, - vec![MessageSegment::Text(edited_text)], - creases, - Some(context.loaded_context), - checkpoint.ok(), - cx, - ); - for message_id in this.messages_after(message_id) { - thread.delete_message(*message_id, cx); - } - }); - - this.thread.update(cx, |thread, cx| { - thread.advance_prompt_id(); - thread.cancel_last_completion(Some(window.window_handle()), cx); - thread.send_to_model( - model.model, - CompletionIntent::UserPrompt, - Some(window.window_handle()), - cx, - ); - }); - this._load_edited_message_context_task = None; - cx.notify(); - }) - .log_err(); - })); - - if let Some(workspace) = self.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = workspace.panel::(cx) { - panel.focus_handle(cx).focus(window); - } - }); - } - } - - fn messages_after(&self, message_id: MessageId) -> &[MessageId] { - self.messages - .iter() - .position(|id| *id == message_id) - .map(|index| &self.messages[index + 1..]) - .unwrap_or(&[]) - } - - fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - self.cancel_editing_message(&menu::Cancel, window, cx); - } - - fn handle_regenerate_click( - &mut self, - _: &ClickEvent, - window: &mut Window, - cx: &mut Context, - ) { - self.confirm_editing_message(&menu::Confirm, window, cx); - } - - fn handle_feedback_click( - &mut self, - message_id: MessageId, - feedback: ThreadFeedback, - window: &mut Window, - cx: &mut Context, - ) { - let report = self.thread.update(cx, |thread, cx| { - thread.report_message_feedback(message_id, feedback, cx) - }); - - cx.spawn(async move |this, cx| { - report.await?; - this.update(cx, |_this, cx| cx.notify()) - }) - .detach_and_log_err(cx); - - match feedback { - ThreadFeedback::Positive => { - self.open_feedback_editors.remove(&message_id); - } - ThreadFeedback::Negative => { - self.handle_show_feedback_comments(message_id, window, cx); - } - } - } - - fn handle_show_feedback_comments( - &mut self, - message_id: MessageId, - window: &mut Window, - cx: &mut Context, - ) { - let buffer = cx.new(|cx| { - let empty_string = String::new(); - MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx) - }); - - let editor = cx.new(|cx| { - let mut editor = Editor::new( - editor::EditorMode::AutoHeight { - min_lines: 1, - max_lines: Some(4), - }, - buffer, - None, - window, - cx, - ); - editor.set_placeholder_text( - "What went wrong? Share your feedback so we can improve.", - window, - cx, - ); - editor - }); - - editor.read(cx).focus_handle(cx).focus(window); - self.open_feedback_editors.insert(message_id, editor); - cx.notify(); - } - - fn submit_feedback_message(&mut self, message_id: MessageId, cx: &mut Context) { - let Some(editor) = self.open_feedback_editors.get(&message_id) else { - return; - }; - - let report_task = self.thread.update(cx, |thread, cx| { - thread.report_message_feedback(message_id, ThreadFeedback::Negative, cx) - }); - - let comments = editor.read(cx).text(cx); - if !comments.is_empty() { - let thread_id = self.thread.read(cx).id().clone(); - let comments_value = String::from(comments.as_str()); - - let message_content = self - .thread - .read(cx) - .message(message_id) - .map(|msg| msg.to_message_content()) - .unwrap_or_default(); - - telemetry::event!( - "Assistant Thread Feedback Comments", - thread_id, - message_id = message_id.as_usize(), - message_content, - comments = comments_value - ); - - self.open_feedback_editors.remove(&message_id); - - cx.spawn(async move |this, cx| { - report_task.await?; - this.update(cx, |_this, cx| cx.notify()) - }) - .detach_and_log_err(cx); - } - } - - fn render_edit_message_editor( - &self, - state: &EditingMessageState, - _window: &mut Window, - cx: &Context, - ) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - 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(); - - let text_style = TextStyle { - color: colors.text, - font_family: settings.buffer_font.family.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_features: settings.buffer_font.features.clone(), - font_size: font_size.into(), - line_height: line_height.into(), - ..Default::default() - }; - - v_flex() - .key_context("EditMessageEditor") - .on_action(cx.listener(Self::toggle_context_picker)) - .on_action(cx.listener(Self::remove_all_context)) - .on_action(cx.listener(Self::move_up)) - .on_action(cx.listener(Self::cancel_editing_message)) - .on_action(cx.listener(Self::confirm_editing_message)) - .capture_action(cx.listener(Self::paste)) - .min_h_6() - .w_full() - .flex_grow() - .gap_2() - .child(state.context_strip.clone()) - .child(div().pt(px(-3.)).px_neg_0p5().child(EditorElement::new( - &state.editor, - EditorStyle { - background: colors.editor_background, - local_player: cx.theme().players().local(), - text: text_style, - syntax: cx.theme().syntax().clone(), - ..Default::default() - }, - ))) - } - - fn render_message( - &mut self, - ix: usize, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let message_id = self.messages[ix]; - let workspace = self.workspace.clone(); - let thread = self.thread.read(cx); - - let is_first_message = ix == 0; - let is_last_message = ix == self.messages.len() - 1; - - let Some(message) = thread.message(message_id) else { - return Empty.into_any(); - }; - - let is_generating = thread.is_generating(); - let is_generating_stale = thread.is_generation_stale().unwrap_or(false); - - let loading_dots = (is_generating && is_last_message).then(|| { - h_flex() - .h_8() - .my_3() - .mx_5() - .when(is_generating_stale || message.is_hidden, |this| { - this.child(LoadingLabel::new("").size(LabelSize::Small)) - }) - }); - - if message.is_hidden { - return div().children(loading_dots).into_any(); - } - - let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else { - return Empty.into_any(); - }; - - // Get all the data we need from thread before we start using it in closures - let checkpoint = thread.checkpoint_for_message(message_id); - let configured_model = thread.configured_model().map(|m| m.model); - let added_context = thread - .context_for_message(message_id) - .map(|context| AddedContext::new_attached(context, configured_model.as_ref(), cx)) - .collect::>(); - - let tool_uses = thread.tool_uses_for_message(message_id, cx); - let has_tool_uses = !tool_uses.is_empty(); - - let editing_message_state = self - .editing_message - .as_ref() - .filter(|(id, _)| *id == message_id) - .map(|(_, state)| state); - - let (editor_bg_color, panel_bg) = { - let colors = cx.theme().colors(); - (colors.editor_background, colors.panel_background) - }; - - let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileMarkdown) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Ignored) - .tooltip(Tooltip::text("Open Thread as Markdown")) - .on_click({ - let thread = self.thread.clone(); - let workspace = self.workspace.clone(); - move |_, window, cx| { - if let Some(workspace) = workspace.upgrade() { - open_active_thread_as_markdown(thread.clone(), workspace, window, cx) - .detach_and_log_err(cx); - } - } - }); - - let scroll_to_top = IconButton::new(("scroll_to_top", ix), IconName::ArrowUp) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Ignored) - .tooltip(Tooltip::text("Scroll To Top")) - .on_click(cx.listener(move |this, _, _, cx| { - this.scroll_to_top(cx); - })); - - let show_feedback = thread.is_turn_end(ix); - let feedback_container = h_flex() - .group("feedback_container") - .mt_1() - .py_2() - .px(RESPONSE_PADDING_X) - .mr_1() - .gap_1() - .opacity(0.4) - .hover(|style| style.opacity(1.)) - .gap_1p5() - .flex_wrap() - .justify_end(); - let feedback_items = match self.thread.read(cx).message_feedback(message_id) { - Some(feedback) => feedback_container - .child( - div().visible_on_hover("feedback_container").child( - Label::new(match feedback { - ThreadFeedback::Positive => "Thanks for your feedback!", - ThreadFeedback::Negative => { - "We appreciate your feedback and will use it to improve." - } - }) - .color(Color::Muted) - .size(LabelSize::XSmall) - .truncate()) - ) - .child( - h_flex() - .child( - IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(match feedback { - ThreadFeedback::Positive => Color::Accent, - ThreadFeedback::Negative => Color::Ignored, - }) - .tooltip(Tooltip::text("Helpful Response")) - .on_click(cx.listener(move |this, _, window, cx| { - this.handle_feedback_click( - message_id, - ThreadFeedback::Positive, - window, - cx, - ); - })), - ) - .child( - IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(match feedback { - ThreadFeedback::Positive => Color::Ignored, - ThreadFeedback::Negative => Color::Accent, - }) - .tooltip(Tooltip::text("Not Helpful")) - .on_click(cx.listener(move |this, _, window, cx| { - this.handle_feedback_click( - message_id, - ThreadFeedback::Negative, - window, - cx, - ); - })), - ) - .child(open_as_markdown), - ) - .into_any_element(), - None if AgentSettings::get_global(cx).enable_feedback => - feedback_container - .child( - div().visible_on_hover("feedback_container").child( - Label::new( - "Rating the thread sends all of your current conversation to the Zed team.", - ) - .color(Color::Muted) - .size(LabelSize::XSmall) - .truncate()) - ) - .child( - h_flex() - .child( - IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Ignored) - .tooltip(Tooltip::text("Helpful Response")) - .on_click(cx.listener(move |this, _, window, cx| { - this.handle_feedback_click( - message_id, - ThreadFeedback::Positive, - window, - cx, - ); - })), - ) - .child( - IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Ignored) - .tooltip(Tooltip::text("Not Helpful")) - .on_click(cx.listener(move |this, _, window, cx| { - this.handle_feedback_click( - message_id, - ThreadFeedback::Negative, - window, - cx, - ); - })), - ) - .child(open_as_markdown) - .child(scroll_to_top), - ) - .into_any_element(), - None => feedback_container - .child(h_flex() - .child(open_as_markdown)) - .child(scroll_to_top) - .into_any_element(), - }; - - let message_is_empty = message.should_display_content(); - let has_content = !message_is_empty || !added_context.is_empty(); - - let message_content = has_content.then(|| { - if let Some(state) = editing_message_state.as_ref() { - self.render_edit_message_editor(state, window, cx) - .into_any_element() - } else { - v_flex() - .w_full() - .gap_1() - .when(!added_context.is_empty(), |parent| { - parent.child(h_flex().flex_wrap().gap_1().children( - added_context.into_iter().map(|added_context| { - let context = added_context.handle.clone(); - ContextPill::added(added_context, false, false, None).on_click( - Rc::new(cx.listener({ - let workspace = workspace.clone(); - move |_, _, window, cx| { - if let Some(workspace) = workspace.upgrade() { - open_context(&context, workspace, window, cx); - cx.notify(); - } - } - })), - ) - }), - )) - }) - .when(!message_is_empty, |parent| { - parent.child(div().pt_0p5().min_h_6().child(self.render_message_content( - message_id, - rendered_message, - has_tool_uses, - workspace.clone(), - window, - cx, - ))) - }) - .into_any_element() - } - }); - - let styled_message = if message.ui_only { - self.render_ui_notification(message_content, ix, cx) - } else { - match message.role { - Role::User => { - let colors = cx.theme().colors(); - v_flex() - .id(("message-container", ix)) - .pt_2() - .pl_2() - .pr_2p5() - .pb_4() - .child( - v_flex() - .id(("user-message", ix)) - .bg(editor_bg_color) - .rounded_lg() - .shadow_md() - .border_1() - .border_color(colors.border) - .hover(|hover| hover.border_color(colors.text_accent.opacity(0.5))) - .child( - v_flex() - .p_2p5() - .gap_1() - .children(message_content) - .when_some(editing_message_state, |this, state| { - let focus_handle = state.editor.focus_handle(cx); - - this.child( - h_flex() - .w_full() - .gap_1() - .justify_between() - .flex_wrap() - .child( - h_flex() - .gap_1p5() - .child( - div() - .opacity(0.8) - .child( - Icon::new(IconName::Warning) - .size(IconSize::Indicator) - .color(Color::Warning) - ), - ) - .child( - Label::new("Editing will restart the thread from this point.") - .color(Color::Muted) - .size(LabelSize::XSmall), - ), - ) - .child( - h_flex() - .gap_0p5() - .child( - IconButton::new( - "cancel-edit-message", - IconName::Close, - ) - .shape(ui::IconButtonShape::Square) - .icon_color(Color::Error) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Cancel Edit", - &menu::Cancel, - &focus_handle, - window, - cx, - ) - } - }) - .on_click(cx.listener(Self::handle_cancel_click)), - ) - .child( - IconButton::new( - "confirm-edit-message", - IconName::Return, - ) - .disabled(state.editor.read(cx).is_empty(cx)) - .shape(ui::IconButtonShape::Square) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .tooltip({ - move |window, cx| { - Tooltip::for_action_in( - "Regenerate", - &menu::Confirm, - &focus_handle, - window, - cx, - ) - } - }) - .on_click( - cx.listener(Self::handle_regenerate_click), - ), - ), - ) - ) - }), - ) - .on_click(cx.listener({ - let message_creases = message.creases.clone(); - move |this, _, window, cx| { - if let Some(message_text) = - this.thread.read(cx).message(message_id).and_then(|message| { - message.segments.first().and_then(|segment| { - match segment { - MessageSegment::Text(message_text) => { - Some(Into::>::into(message_text.as_str())) - } - _ => { - None - } - } - }) - }) - { - this.start_editing_message( - message_id, - message_text, - &message_creases, - window, - cx, - ); - } - } - })), - ) - } - Role::Assistant => v_flex() - .id(("message-container", ix)) - .px(RESPONSE_PADDING_X) - .gap_2() - .children(message_content) - .when(has_tool_uses, |parent| { - parent.children(tool_uses.into_iter().map(|tool_use| { - self.render_tool_use(tool_use, window, workspace.clone(), cx) - })) - }), - Role::System => { - let colors = cx.theme().colors(); - div().id(("message-container", ix)).py_1().px_2().child( - v_flex() - .bg(colors.editor_background) - .rounded_sm() - .child(div().p_4().children(message_content)), - ) - } - } - }; - - let after_editing_message = self - .editing_message - .as_ref() - .is_some_and(|(editing_message_id, _)| message_id > *editing_message_id); - - let backdrop = div() - .id(("backdrop", ix)) - .size_full() - .absolute() - .inset_0() - .bg(panel_bg) - .opacity(0.8) - .block_mouse_except_scroll() - .on_click(cx.listener(Self::handle_cancel_click)); - - 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() - && 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 = 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)) - .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) - .children(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(); - })) - .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(backdrop) - }) - .into_any() - } - - fn render_message_content( - &self, - message_id: MessageId, - rendered_message: &RenderedMessage, - has_tool_uses: bool, - workspace: WeakEntity, - window: &Window, - cx: &Context, - ) -> impl IntoElement { - let is_last_message = self.messages.last() == Some(&message_id); - let is_generating = self.thread.read(cx).is_generating(); - let pending_thinking_segment_index = if is_generating && is_last_message && !has_tool_uses { - rendered_message - .segments - .iter() - .enumerate() - .next_back() - .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. })) - .map(|(index, _)| index) - } else { - None - }; - - let message_role = self - .thread - .read(cx) - .message(message_id) - .map(|m| m.role) - .unwrap_or(Role::User); - - let is_assistant_message = message_role == Role::Assistant; - let is_user_message = message_role == Role::User; - - v_flex() - .text_ui(cx) - .gap_2() - .when(is_user_message, |this| this.text_xs()) - .children( - rendered_message.segments.iter().enumerate().map( - |(index, segment)| match segment { - RenderedMessageSegment::Thinking { - content, - scroll_handle, - } => self - .render_message_thinking_segment( - message_id, - index, - content.clone(), - scroll_handle, - Some(index) == pending_thinking_segment_index, - window, - cx, - ) - .into_any_element(), - RenderedMessageSegment::Text(markdown) => { - let markdown_element = MarkdownElement::new( - markdown.clone(), - if is_user_message { - let mut style = default_markdown_style(window, cx); - let mut text_style = window.text_style(); - let theme_settings = ThemeSettings::get_global(cx); - - let buffer_font = theme_settings.buffer_font.family.clone(); - let buffer_font_size = TextSize::Small.rems(cx); - - text_style.refine(&TextStyleRefinement { - font_family: Some(buffer_font), - font_size: Some(buffer_font_size.into()), - ..Default::default() - }); - - style.base_text_style = text_style; - style - } else { - default_markdown_style(window, cx) - }, - ); - - let markdown_element = if is_assistant_message { - markdown_element.code_block_renderer( - markdown::CodeBlockRenderer::Custom { - render: Arc::new({ - let workspace = workspace.clone(); - let active_thread = cx.entity(); - move |kind, - parsed_markdown, - range, - metadata, - window, - cx| { - render_markdown_code_block( - message_id, - range.start, - kind, - parsed_markdown, - metadata, - active_thread.clone(), - workspace.clone(), - window, - cx, - ) - } - }), - transform: Some(Arc::new({ - let active_thread = cx.entity(); - - move |element, range, _, _, cx| { - let is_expanded = active_thread - .read(cx) - .is_codeblock_expanded(message_id, range.start); - - if is_expanded { - return element; - } - - element - } - })), - }, - ) - } else { - markdown_element.code_block_renderer( - markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, - border: true, - }, - ) - }; - - div() - .child(markdown_element.on_url_click({ - let workspace = self.workspace.clone(); - move |text, window, cx| { - open_markdown_link(text, workspace.clone(), window, cx); - } - })) - .into_any_element() - } - }, - ), - ) - } - - fn tool_card_border_color(&self, cx: &Context) -> Hsla { - cx.theme().colors().border.opacity(0.5) - } - - fn tool_card_header_bg(&self, cx: &Context) -> Hsla { - cx.theme() - .colors() - .element_background - .blend(cx.theme().colors().editor_foreground.opacity(0.025)) - } - - fn render_ui_notification( - &self, - message_content: impl IntoIterator, - ix: usize, - cx: &mut Context, - ) -> Stateful
{ - let message = div() - .flex_1() - .min_w_0() - .text_size(TextSize::XSmall.rems(cx)) - .text_color(cx.theme().colors().text_muted) - .children(message_content); - - div() - .id(("message-container", ix)) - .py_1() - .px_2p5() - .child(Banner::new().severity(Severity::Warning).child(message)) - } - - fn render_message_thinking_segment( - &self, - message_id: MessageId, - ix: usize, - markdown: Entity, - scroll_handle: &ScrollHandle, - pending: bool, - window: &Window, - cx: &Context, - ) -> impl IntoElement { - let is_open = self - .expanded_thinking_segments - .get(&(message_id, ix)) - .copied() - .unwrap_or_default(); - - let editor_bg = cx.theme().colors().panel_background; - - div().map(|this| { - if pending { - this.v_flex() - .mt_neg_2() - .mb_1p5() - .child( - h_flex() - .group("disclosure-header") - .justify_between() - .child( - h_flex() - .gap_1p5() - .child( - Icon::new(IconName::ToolThink) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child(LoadingLabel::new("Thinking").size(LabelSize::Small)), - ) - .child( - h_flex() - .gap_1() - .child( - div().visible_on_hover("disclosure-header").child( - Disclosure::new("thinking-disclosure", is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener({ - move |this, _event, _window, _cx| { - let is_open = this - .expanded_thinking_segments - .entry((message_id, ix)) - .or_insert(false); - - *is_open = !*is_open; - } - })), - ), - ) - .child({ - Icon::new(IconName::ArrowCircle) - .color(Color::Accent) - .size(IconSize::Small) - .with_rotate_animation(2) - }), - ), - ) - .when(!is_open, |this| { - let gradient_overlay = div() - .rounded_b_lg() - .h_full() - .absolute() - .w_full() - .bottom_0() - .left_0() - .bg(linear_gradient( - 180., - linear_color_stop(editor_bg, 1.), - linear_color_stop(editor_bg.opacity(0.2), 0.), - )); - - this.child( - div() - .relative() - .bg(editor_bg) - .rounded_b_lg() - .mt_2() - .pl_4() - .child( - div() - .id(("thinking-content", ix)) - .max_h_20() - .track_scroll(scroll_handle) - .text_ui_sm(cx) - .overflow_hidden() - .child( - MarkdownElement::new( - markdown.clone(), - default_markdown_style(window, cx), - ) - .on_url_click({ - let workspace = self.workspace.clone(); - move |text, window, cx| { - open_markdown_link( - text, - workspace.clone(), - window, - cx, - ); - } - }), - ), - ) - .child(gradient_overlay), - ) - }) - .when(is_open, |this| { - this.child( - div() - .id(("thinking-content", ix)) - .h_full() - .bg(editor_bg) - .text_ui_sm(cx) - .child( - MarkdownElement::new( - markdown.clone(), - default_markdown_style(window, cx), - ) - .on_url_click({ - let workspace = self.workspace.clone(); - move |text, window, cx| { - open_markdown_link(text, workspace.clone(), window, cx); - } - }), - ), - ) - }) - } else { - this.v_flex() - .mt_neg_2() - .child( - h_flex() - .group("disclosure-header") - .pr_1() - .justify_between() - .opacity(0.8) - .hover(|style| style.opacity(1.)) - .child( - h_flex() - .gap_1p5() - .child( - Icon::new(IconName::ToolThink) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child(Label::new("Thought Process").size(LabelSize::Small)), - ) - .child( - div().visible_on_hover("disclosure-header").child( - Disclosure::new("thinking-disclosure", is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener({ - move |this, _event, _window, _cx| { - let is_open = this - .expanded_thinking_segments - .entry((message_id, ix)) - .or_insert(false); - - *is_open = !*is_open; - } - })), - ), - ), - ) - .child( - div() - .id(("thinking-content", ix)) - .relative() - .mt_1p5() - .ml_1p5() - .pl_2p5() - .border_l_1() - .border_color(cx.theme().colors().border_variant) - .text_ui_sm(cx) - .when(is_open, |this| { - this.child( - MarkdownElement::new( - markdown.clone(), - default_markdown_style(window, cx), - ) - .on_url_click({ - let workspace = self.workspace.clone(); - move |text, window, cx| { - open_markdown_link(text, workspace.clone(), window, cx); - } - }), - ) - }), - ) - } - }) - } - - fn render_tool_use( - &self, - tool_use: ToolUse, - window: &mut Window, - workspace: WeakEntity, - cx: &mut Context, - ) -> impl IntoElement + use<> { - if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) { - return card.render(&tool_use.status, window, workspace, cx); - } - - let is_open = self - .expanded_tool_uses - .get(&tool_use.id) - .copied() - .unwrap_or_default(); - - let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_)); - - let fs = self - .workspace - .upgrade() - .map(|workspace| workspace.read(cx).app_state().fs.clone()); - let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation); - let needs_confirmation_tools = tool_use.needs_confirmation; - - let status_icons = div().child(match &tool_use.status { - ToolUseStatus::NeedsConfirmation => { - let icon = Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::Small); - icon.into_any_element() - } - ToolUseStatus::Pending - | ToolUseStatus::InputStillStreaming - | ToolUseStatus::Running => Icon::new(IconName::ArrowCircle) - .color(Color::Accent) - .size(IconSize::Small) - .with_rotate_animation(2) - .into_any_element(), - ToolUseStatus::Finished(_) => div().w_0().into_any_element(), - ToolUseStatus::Error(_) => { - let icon = Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small); - icon.into_any_element() - } - }); - - let rendered_tool_use = self.rendered_tool_uses.get(&tool_use.id).cloned(); - let results_content_container = || v_flex().p_2().gap_0p5(); - - let results_content = v_flex() - .gap_1() - .child( - results_content_container() - .child( - Label::new("Input") - .size(LabelSize::XSmall) - .color(Color::Muted) - .buffer_font(cx), - ) - .child( - div() - .w_full() - .text_ui_sm(cx) - .children(rendered_tool_use.as_ref().map(|rendered| { - MarkdownElement::new( - rendered.input.clone(), - tool_use_markdown_style(window, cx), - ) - .code_block_renderer(markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, - border: false, - }) - .on_url_click({ - let workspace = self.workspace.clone(); - move |text, window, cx| { - open_markdown_link(text, workspace.clone(), window, cx); - } - }) - })), - ), - ) - .map(|container| match tool_use.status { - ToolUseStatus::Finished(_) => container.child( - results_content_container() - .border_t_1() - .border_color(self.tool_card_border_color(cx)) - .child( - Label::new("Result") - .size(LabelSize::XSmall) - .color(Color::Muted) - .buffer_font(cx), - ) - .child(div().w_full().text_ui_sm(cx).children( - rendered_tool_use.as_ref().map(|rendered| { - MarkdownElement::new( - rendered.output.clone(), - tool_use_markdown_style(window, cx), - ) - .code_block_renderer(markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, - border: false, - }) - .on_url_click({ - let workspace = self.workspace.clone(); - move |text, window, cx| { - open_markdown_link(text, workspace.clone(), window, cx); - } - }) - .into_any_element() - }), - )), - ), - ToolUseStatus::InputStillStreaming | ToolUseStatus::Running => container.child( - results_content_container() - .border_t_1() - .border_color(self.tool_card_border_color(cx)) - .child( - h_flex() - .gap_1() - .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .color(Color::Accent) - .with_rotate_animation(2), - ) - .child( - Label::new("Running…") - .size(LabelSize::XSmall) - .color(Color::Muted) - .buffer_font(cx), - ), - ), - ), - ToolUseStatus::Error(_) => container.child( - results_content_container() - .border_t_1() - .border_color(self.tool_card_border_color(cx)) - .child( - Label::new("Error") - .size(LabelSize::XSmall) - .color(Color::Muted) - .buffer_font(cx), - ) - .child( - div() - .text_ui_sm(cx) - .children(rendered_tool_use.as_ref().map(|rendered| { - MarkdownElement::new( - rendered.output.clone(), - tool_use_markdown_style(window, cx), - ) - .on_url_click({ - let workspace = self.workspace.clone(); - move |text, window, cx| { - open_markdown_link(text, workspace.clone(), window, cx); - } - }) - .into_any_element() - })), - ), - ), - ToolUseStatus::Pending => container, - ToolUseStatus::NeedsConfirmation => container.child( - results_content_container() - .border_t_1() - .border_color(self.tool_card_border_color(cx)) - .child( - Label::new("Asking Permission") - .size(LabelSize::Small) - .color(Color::Muted) - .buffer_font(cx), - ), - ), - }); - - let gradient_overlay = |color: Hsla| { - div() - .h_full() - .absolute() - .w_12() - .bottom_0() - .map(|element| { - if is_status_finished { - element.right_6() - } else { - element.right(px(44.)) - } - }) - .bg(linear_gradient( - 90., - linear_color_stop(color, 1.), - linear_color_stop(color.opacity(0.2), 0.), - )) - }; - - v_flex().gap_1().mb_2().map(|element| { - if !needs_confirmation_tools { - element.child( - v_flex() - .child( - h_flex() - .group("disclosure-header") - .relative() - .gap_1p5() - .justify_between() - .opacity(0.8) - .hover(|style| style.opacity(1.)) - .when(!is_status_finished, |this| this.pr_2()) - .child( - h_flex() - .id("tool-label-container") - .gap_1p5() - .max_w_full() - .overflow_x_scroll() - .child( - Icon::new(tool_use.icon) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child( - h_flex().pr_8().text_size(rems(0.8125)).children( - rendered_tool_use.map(|rendered| MarkdownElement::new(rendered.label, tool_use_markdown_style(window, cx)).on_url_click({let workspace = self.workspace.clone(); move |text, window, cx| { - open_markdown_link(text, workspace.clone(), window, cx); - }})) - ), - ), - ) - .child( - h_flex() - .gap_1() - .child( - div().visible_on_hover("disclosure-header").child( - Disclosure::new("tool-use-disclosure", is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener({ - let tool_use_id = tool_use.id.clone(); - move |this, _event, _window, _cx| { - let is_open = this - .expanded_tool_uses - .entry(tool_use_id.clone()) - .or_insert(false); - - *is_open = !*is_open; - } - })), - ), - ) - .child(status_icons), - ) - .child(gradient_overlay(cx.theme().colors().panel_background)), - ) - .map(|parent| { - if !is_open { - return parent; - } - - parent.child( - v_flex() - .mt_1() - .border_1() - .border_color(self.tool_card_border_color(cx)) - .bg(cx.theme().colors().editor_background) - .rounded_lg() - .child(results_content), - ) - }), - ) - } else { - v_flex() - .mb_2() - .rounded_lg() - .border_1() - .border_color(self.tool_card_border_color(cx)) - .overflow_hidden() - .child( - h_flex() - .group("disclosure-header") - .relative() - .justify_between() - .py_1() - .map(|element| { - if is_status_finished { - element.pl_2().pr_0p5() - } else { - element.px_2() - } - }) - .bg(self.tool_card_header_bg(cx)) - .map(|element| { - if is_open { - element.border_b_1().rounded_t_md() - } else if needs_confirmation { - element.rounded_t_md() - } else { - element.rounded_md() - } - }) - .border_color(self.tool_card_border_color(cx)) - .child( - h_flex() - .id("tool-label-container") - .gap_1p5() - .max_w_full() - .overflow_x_scroll() - .child( - Icon::new(tool_use.icon) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child( - h_flex().pr_8().text_ui_sm(cx).children( - rendered_tool_use.map(|rendered| MarkdownElement::new(rendered.label, tool_use_markdown_style(window, cx)).on_url_click({let workspace = self.workspace.clone(); move |text, window, cx| { - open_markdown_link(text, workspace.clone(), window, cx); - }})) - ), - ), - ) - .child( - h_flex() - .gap_1() - .child( - div().visible_on_hover("disclosure-header").child( - Disclosure::new("tool-use-disclosure", is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener({ - let tool_use_id = tool_use.id.clone(); - move |this, _event, _window, _cx| { - let is_open = this - .expanded_tool_uses - .entry(tool_use_id.clone()) - .or_insert(false); - - *is_open = !*is_open; - } - })), - ), - ) - .child(status_icons), - ) - .child(gradient_overlay(self.tool_card_header_bg(cx))), - ) - .map(|parent| { - if !is_open { - return parent; - } - - parent.child( - v_flex() - .bg(cx.theme().colors().editor_background) - .map(|element| { - if needs_confirmation { - element.rounded_none() - } else { - element.rounded_b_lg() - } - }) - .child(results_content), - ) - }) - .when(needs_confirmation, |this| { - this.child( - h_flex() - .py_1() - .pl_2() - .pr_1() - .gap_1() - .justify_between() - .flex_wrap() - .bg(cx.theme().colors().editor_background) - .border_t_1() - .border_color(self.tool_card_border_color(cx)) - .rounded_b_lg() - .child( - div() - .min_w(rems_from_px(145.)) - .child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small) - ) - ) - .child( - h_flex() - .gap_0p5() - .child({ - let tool_id = tool_use.id.clone(); - Button::new( - "always-allow-tool-action", - "Always Allow", - ) - .label_size(LabelSize::Small) - .icon(IconName::CheckDouble) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Success) - .tooltip(move |window, cx| { - Tooltip::with_meta( - "Never ask for permission", - None, - "Restore the original behavior in your Agent Panel settings", - window, - cx, - ) - }) - .on_click(cx.listener( - move |this, event, window, cx| { - if let Some(fs) = fs.clone() { - update_settings_file::( - fs.clone(), - cx, - |settings, _| { - settings.set_always_allow_tool_actions(true); - }, - ); - } - this.handle_allow_tool( - tool_id.clone(), - event, - window, - cx, - ) - }, - )) - }) - .child({ - let tool_id = tool_use.id.clone(); - Button::new("allow-tool-action", "Allow") - .label_size(LabelSize::Small) - .icon(IconName::Check) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Success) - .on_click(cx.listener( - move |this, event, window, cx| { - this.handle_allow_tool( - tool_id.clone(), - event, - window, - cx, - ) - }, - )) - }) - .child({ - let tool_id = tool_use.id.clone(); - let tool_name: Arc = tool_use.name.into(); - Button::new("deny-tool", "Deny") - .label_size(LabelSize::Small) - .icon(IconName::Close) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Error) - .on_click(cx.listener( - move |this, event, window, cx| { - this.handle_deny_tool( - tool_id.clone(), - tool_name.clone(), - event, - window, - cx, - ) - }, - )) - }), - ), - ) - }) - } - }).into_any_element() - } - - fn render_rules_item(&self, cx: &Context) -> AnyElement { - let project_context = self.thread.read(cx).project_context(); - let project_context = project_context.borrow(); - let Some(project_context) = project_context.as_ref() else { - return div().into_any(); - }; - - let user_rules_text = if project_context.user_rules.is_empty() { - None - } else if project_context.user_rules.len() == 1 { - let user_rules = &project_context.user_rules[0]; - - match user_rules.title.as_ref() { - Some(title) => Some(format!("Using \"{title}\" user rule")), - None => Some("Using user rule".into()), - } - } else { - Some(format!( - "Using {} user rules", - project_context.user_rules.len() - )) - }; - - let first_user_rules_id = project_context - .user_rules - .first() - .map(|user_rules| user_rules.uuid.0); - - let rules_files = project_context - .worktrees - .iter() - .filter_map(|worktree| worktree.rules_file.as_ref()) - .collect::>(); - - let rules_file_text = match rules_files.as_slice() { - &[] => None, - &[rules_file] => Some(format!( - "Using project {:?} file", - rules_file.path_in_worktree - )), - rules_files => Some(format!("Using {} project rules files", rules_files.len())), - }; - - if user_rules_text.is_none() && rules_file_text.is_none() { - return div().into_any(); - } - - v_flex() - .pt_2() - .px_2p5() - .gap_1() - .when_some(user_rules_text, |parent, user_rules_text| { - parent.child( - h_flex() - .w_full() - .child( - Icon::new(RULES_ICON) - .size(IconSize::XSmall) - .color(Color::Disabled), - ) - .child( - Label::new(user_rules_text) - .size(LabelSize::XSmall) - .color(Color::Muted) - .truncate() - .buffer_font(cx) - .ml_1p5() - .mr_0p5(), - ) - .child( - IconButton::new("open-prompt-library", IconName::ArrowUpRight) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Ignored) - // TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary` keybinding - .tooltip(Tooltip::text("View User Rules")) - .on_click(move |_event, window, cx| { - window.dispatch_action( - Box::new(OpenRulesLibrary { - prompt_to_select: first_user_rules_id, - }), - cx, - ) - }), - ), - ) - }) - .when_some(rules_file_text, |parent, rules_file_text| { - parent.child( - h_flex() - .w_full() - .child( - Icon::new(IconName::File) - .size(IconSize::XSmall) - .color(Color::Disabled), - ) - .child( - Label::new(rules_file_text) - .size(LabelSize::XSmall) - .color(Color::Muted) - .buffer_font(cx) - .ml_1p5() - .mr_0p5(), - ) - .child( - IconButton::new("open-rule", IconName::ArrowUpRight) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Ignored) - .on_click(cx.listener(Self::handle_open_rules)) - .tooltip(Tooltip::text("View Rules")), - ), - ) - }) - .into_any() - } - - fn handle_allow_tool( - &mut self, - tool_use_id: LanguageModelToolUseId, - _: &ClickEvent, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self - .thread - .read(cx) - .pending_tool(&tool_use_id) - .map(|tool_use| tool_use.status.clone()) - { - self.thread.update(cx, |thread, cx| { - if let Some(configured) = thread.get_or_init_configured_model(cx) { - thread.run_tool( - c.tool_use_id.clone(), - c.ui_text.clone(), - c.input.clone(), - c.request.clone(), - c.tool.clone(), - configured.model, - Some(window.window_handle()), - cx, - ); - } - }); - } - } - - fn handle_deny_tool( - &mut self, - tool_use_id: LanguageModelToolUseId, - tool_name: Arc, - _: &ClickEvent, - window: &mut Window, - cx: &mut Context, - ) { - let window_handle = window.window_handle(); - self.thread.update(cx, |thread, cx| { - thread.deny_tool_use(tool_use_id, tool_name, Some(window_handle), cx); - }); - } - - fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - let project_context = self.thread.read(cx).project_context(); - let project_context = project_context.borrow(); - let Some(project_context) = project_context.as_ref() else { - return; - }; - - let project_entry_ids = project_context - .worktrees - .iter() - .flat_map(|worktree| worktree.rules_file.as_ref()) - .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id)) - .collect::>(); - - self.workspace - .update(cx, move |workspace, cx| { - // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules - // files clear. For example, if rules file 1 is already open but rules file 2 is not, - // this would open and focus rules file 2 in a tab that is not next to rules file 1. - let project = workspace.project().read(cx); - let project_paths = project_entry_ids - .into_iter() - .flat_map(|entry_id| project.path_for_entry(entry_id, cx)) - .collect::>(); - for project_path in project_paths { - workspace - .open_path(project_path, None, true, window, cx) - .detach_and_log_err(cx); - } - }) - .ok(); - } - - fn dismiss_notifications(&mut self, cx: &mut Context) { - for window in self.notifications.drain(..) { - window - .update(cx, |_, window, _| { - window.remove_window(); - }) - .ok(); - - self.notification_subscriptions.remove(&window); - } - } - - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("active-thread-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) - } - - pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool { - self.expanded_code_blocks - .get(&(message_id, ix)) - .copied() - .unwrap_or(true) - } - - pub fn toggle_codeblock_expanded(&mut self, message_id: MessageId, ix: usize) { - let is_expanded = self - .expanded_code_blocks - .entry((message_id, ix)) - .or_insert(true); - *is_expanded = !*is_expanded; - } - - pub fn scroll_to_top(&mut self, cx: &mut Context) { - self.list_state.scroll_to(ListOffset::default()); - cx.notify(); - } - - pub fn scroll_to_bottom(&mut self, cx: &mut Context) { - self.list_state.reset(self.messages.len()); - cx.notify(); - } -} - -pub enum ActiveThreadEvent { - EditingMessageTokenCountChanged, -} - -impl EventEmitter for ActiveThread {} - -impl Render for ActiveThread { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .size_full() - .relative() - .bg(cx.theme().colors().panel_background) - .child(list(self.list_state.clone(), cx.processor(Self::render_message)).flex_grow()) - .child(self.render_vertical_scrollbar(cx)) - } -} - -pub(crate) fn open_active_thread_as_markdown( - thread: Entity, - workspace: Entity, - window: &mut Window, - cx: &mut App, -) -> Task> { - let markdown_language_task = workspace - .read(cx) - .app_state() - .languages - .language_for_name("Markdown"); - - window.spawn(cx, async move |cx| { - let markdown_language = markdown_language_task.await?; - - workspace.update_in(cx, |workspace, window, cx| { - let thread = thread.read(cx); - let markdown = thread.to_markdown(cx)?; - let thread_summary = thread.summary().or_default().to_string(); - - let project = workspace.project().clone(); - - if !project.read(cx).is_local() { - anyhow::bail!("failed to open active thread as markdown in remote project"); - } - - let buffer = project.update(cx, |project, cx| { - project.create_local_buffer(&markdown, Some(markdown_language), true, cx) - }); - let buffer = - cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())); - - workspace.add_item_to_active_pane( - Box::new(cx.new(|cx| { - let mut editor = - Editor::for_multibuffer(buffer, Some(project.clone()), window, cx); - editor.set_breadcrumb_header(thread_summary); - editor - })), - None, - true, - window, - cx, - ); - - anyhow::Ok(()) - })??; - anyhow::Ok(()) - }) -} - -pub(crate) fn open_context( - context: &AgentContextHandle, - workspace: Entity, - window: &mut Window, - cx: &mut App, -) { - match context { - AgentContextHandle::File(file_context) => { - if let Some(project_path) = file_context.project_path(cx) { - workspace.update(cx, |workspace, cx| { - workspace - .open_path(project_path, None, true, window, cx) - .detach_and_log_err(cx); - }); - } - } - - AgentContextHandle::Directory(directory_context) => { - let entry_id = directory_context.entry_id; - workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |_project, cx| { - cx.emit(project::Event::RevealInProjectPanel(entry_id)); - }) - }) - } - - AgentContextHandle::Symbol(symbol_context) => { - let buffer = symbol_context.buffer.read(cx); - if let Some(project_path) = buffer.project_path(cx) { - let snapshot = buffer.snapshot(); - let target_position = symbol_context.range.start.to_point(&snapshot); - open_editor_at_position(project_path, target_position, &workspace, window, cx) - .detach(); - } - } - - AgentContextHandle::Selection(selection_context) => { - let buffer = selection_context.buffer.read(cx); - if let Some(project_path) = buffer.project_path(cx) { - let snapshot = buffer.snapshot(); - let target_position = selection_context.range.start.to_point(&snapshot); - - open_editor_at_position(project_path, target_position, &workspace, window, cx) - .detach(); - } - } - - AgentContextHandle::FetchedUrl(fetched_url_context) => { - cx.open_url(&fetched_url_context.url); - } - - AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| { - if let Some(panel) = workspace.panel::(cx) { - let thread = thread_context.thread.clone(); - window.defer(cx, move |window, cx| { - panel.update(cx, |panel, cx| { - panel.open_thread(thread, window, cx); - }); - }); - } - }), - - AgentContextHandle::TextThread(text_thread_context) => { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = workspace.panel::(cx) { - let context = text_thread_context.context.clone(); - window.defer(cx, move |window, cx| { - panel.update(cx, |panel, cx| { - panel.open_prompt_editor(context, window, cx) - }); - }); - } - }) - } - - AgentContextHandle::Rules(rules_context) => window.dispatch_action( - Box::new(OpenRulesLibrary { - prompt_to_select: Some(rules_context.prompt_id.0), - }), - cx, - ), - - AgentContextHandle::Image(_) => {} - } -} - -pub(crate) fn attach_pasted_images_as_context( - context_store: &Entity, - cx: &mut App, -) -> bool { - let images = cx - .read_from_clipboard() - .map(|item| { - item.into_entries() - .filter_map(|entry| { - if let ClipboardEntry::Image(image) = entry { - Some(image) - } else { - None - } - }) - .collect::>() - }) - .unwrap_or_default(); - - if images.is_empty() { - return false; - } - cx.stop_propagation(); - - context_store.update(cx, |store, cx| { - for image in images { - store.add_image_instance(Arc::new(image), cx); - } - }); - true -} - -fn open_editor_at_position( - project_path: project::ProjectPath, - target_position: Point, - workspace: &Entity, - window: &mut Window, - cx: &mut App, -) -> Task<()> { - let open_task = workspace.update(cx, |workspace, cx| { - workspace.open_path(project_path, None, true, window, cx) - }); - window.spawn(cx, async move |cx| { - if let Some(active_editor) = open_task - .await - .log_err() - .and_then(|item| item.downcast::()) - { - active_editor - .downgrade() - .update_in(cx, |editor, window, cx| { - editor.go_to_singleton_buffer_point(target_position, window, cx); - }) - .log_err(); - } - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use agent::{MessageSegment, context::ContextLoadResult, thread_store}; - use assistant_tool::{ToolRegistry, ToolWorkingSet}; - use editor::EditorSettings; - use fs::FakeFs; - use gpui::{AppContext, TestAppContext, VisualTestContext}; - use language_model::{ - ConfiguredModel, LanguageModel, LanguageModelRegistry, - fake_provider::{FakeLanguageModel, FakeLanguageModelProvider}, - }; - use project::Project; - use prompt_store::PromptBuilder; - use serde_json::json; - use settings::SettingsStore; - use util::path; - use workspace::CollaboratorId; - - #[gpui::test] - async fn test_agent_is_unfollowed_after_cancelling_completion(cx: &mut TestAppContext) { - init_test_settings(cx); - - let project = create_test_project( - cx, - json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}), - ) - .await; - - let (cx, _active_thread, workspace, thread, model) = - setup_test_environment(cx, project.clone()).await; - - // Insert user message without any context (empty context vector) - thread.update(cx, |thread, cx| { - thread.insert_user_message( - "What is the best way to learn Rust?", - ContextLoadResult::default(), - None, - vec![], - cx, - ); - }); - - // Stream response to user message - thread.update(cx, |thread, cx| { - let intent = CompletionIntent::UserPrompt; - let request = thread.to_completion_request(model.clone(), intent, cx); - thread.stream_completion(request, model, intent, cx.active_window(), cx) - }); - // Follow the agent - cx.update(|window, cx| { - workspace.update(cx, |workspace, cx| { - workspace.follow(CollaboratorId::Agent, window, cx); - }) - }); - assert!(cx.read(|cx| workspace.read(cx).is_being_followed(CollaboratorId::Agent))); - - // Cancel the current completion - thread.update(cx, |thread, cx| { - thread.cancel_last_completion(cx.active_window(), cx) - }); - - cx.executor().run_until_parked(); - - // No longer following the agent - assert!(!cx.read(|cx| workspace.read(cx).is_being_followed(CollaboratorId::Agent))); - } - - #[gpui::test] - async fn test_reinserting_creases_for_edited_message(cx: &mut TestAppContext) { - init_test_settings(cx); - - let project = create_test_project(cx, json!({})).await; - - let (cx, active_thread, _, thread, model) = - setup_test_environment(cx, project.clone()).await; - cx.update(|_, cx| { - LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - registry.set_default_model( - Some(ConfiguredModel { - provider: Arc::new(FakeLanguageModelProvider::default()), - model, - }), - cx, - ); - }); - }); - - let creases = vec![MessageCrease { - range: 14..22, - icon_path: "icon".into(), - label: "foo.txt".into(), - context: None, - }]; - - let message = thread.update(cx, |thread, cx| { - let message_id = thread.insert_user_message( - "Tell me about @foo.txt", - ContextLoadResult::default(), - None, - creases, - cx, - ); - thread.message(message_id).cloned().unwrap() - }); - - active_thread.update_in(cx, |active_thread, window, cx| { - if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) { - active_thread.start_editing_message( - message.id, - message_text, - message.creases.as_slice(), - window, - cx, - ); - } - let editor = active_thread - .editing_message - .as_ref() - .unwrap() - .1 - .editor - .clone(); - editor.update(cx, |editor, cx| editor.edit([(0..13, "modified")], cx)); - active_thread.confirm_editing_message(&Default::default(), window, cx); - }); - cx.run_until_parked(); - - let message = thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap()); - active_thread.update_in(cx, |active_thread, window, cx| { - if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) { - active_thread.start_editing_message( - message.id, - message_text, - message.creases.as_slice(), - window, - cx, - ); - } - let editor = active_thread - .editing_message - .as_ref() - .unwrap() - .1 - .editor - .clone(); - let text = editor.update(cx, |editor, cx| editor.text(cx)); - assert_eq!(text, "modified @foo.txt"); - }); - } - - #[gpui::test] - async fn test_editing_message_cancels_previous_completion(cx: &mut TestAppContext) { - init_test_settings(cx); - - let project = create_test_project(cx, json!({})).await; - - let (cx, active_thread, _, thread, model) = - setup_test_environment(cx, project.clone()).await; - - cx.update(|_, cx| { - LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - registry.set_default_model( - Some(ConfiguredModel { - provider: Arc::new(FakeLanguageModelProvider::default()), - model: model.clone(), - }), - cx, - ); - }); - }); - - // Track thread events to verify cancellation - let cancellation_events = Arc::new(std::sync::Mutex::new(Vec::new())); - let new_request_events = Arc::new(std::sync::Mutex::new(Vec::new())); - - let _subscription = cx.update(|_, cx| { - let cancellation_events = cancellation_events.clone(); - let new_request_events = new_request_events.clone(); - cx.subscribe( - &thread, - move |_thread, event: &ThreadEvent, _cx| match event { - ThreadEvent::CompletionCanceled => { - cancellation_events.lock().unwrap().push(()); - } - ThreadEvent::NewRequest => { - new_request_events.lock().unwrap().push(()); - } - _ => {} - }, - ) - }); - - // Insert a user message and start streaming a response - let message = thread.update(cx, |thread, cx| { - let message_id = thread.insert_user_message( - "Hello, how are you?", - ContextLoadResult::default(), - None, - vec![], - cx, - ); - thread.advance_prompt_id(); - thread.send_to_model( - model.clone(), - CompletionIntent::UserPrompt, - cx.active_window(), - cx, - ); - thread.message(message_id).cloned().unwrap() - }); - - cx.run_until_parked(); - - // Verify that a completion is in progress - assert!(cx.read(|cx| thread.read(cx).is_generating())); - assert_eq!(new_request_events.lock().unwrap().len(), 1); - - // Edit the message while the completion is still running - active_thread.update_in(cx, |active_thread, window, cx| { - if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) { - active_thread.start_editing_message( - message.id, - message_text, - message.creases.as_slice(), - window, - cx, - ); - } - let editor = active_thread - .editing_message - .as_ref() - .unwrap() - .1 - .editor - .clone(); - editor.update(cx, |editor, cx| { - editor.set_text("What is the weather like?", window, cx); - }); - active_thread.confirm_editing_message(&Default::default(), window, cx); - }); - - cx.run_until_parked(); - - // Verify that the previous completion was canceled - assert_eq!(cancellation_events.lock().unwrap().len(), 1); - - // Verify that a new request was started after cancellation - assert_eq!(new_request_events.lock().unwrap().len(), 2); - - // Verify that the edited message contains the new text - let edited_message = - thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap()); - match &edited_message.segments[0] { - MessageSegment::Text(text) => { - assert_eq!(text, "What is the weather like?"); - } - _ => panic!("Expected text segment"), - } - } - - fn init_test_settings(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - Project::init_settings(cx); - AgentSettings::register(cx); - prompt_store::init(cx); - thread_store::init(cx); - workspace::init_settings(cx); - language_model::init_settings(cx); - ThemeSettings::register(cx); - EditorSettings::register(cx); - ToolRegistry::default_global(cx); - }); - } - - // Helper to create a test project with test files - async fn create_test_project( - cx: &mut TestAppContext, - files: serde_json::Value, - ) -> Entity { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/test"), files).await; - Project::test(fs, [path!("/test").as_ref()], cx).await - } - - async fn setup_test_environment( - cx: &mut TestAppContext, - project: Entity, - ) -> ( - &mut VisualTestContext, - Entity, - Entity, - Entity, - Arc, - ) { - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let thread_store = cx - .update(|_, cx| { - ThreadStore::load( - project.clone(), - cx.new(|_| ToolWorkingSet::default()), - None, - Arc::new(PromptBuilder::new(None).unwrap()), - cx, - ) - }) - .await - .unwrap(); - - let text_thread_store = cx - .update(|_, cx| { - TextThreadStore::new( - project.clone(), - Arc::new(PromptBuilder::new(None).unwrap()), - Default::default(), - cx, - ) - }) - .await - .unwrap(); - - let thread = thread_store.update(cx, |store, cx| store.create_thread(cx)); - let context_store = - cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade()))); - - let model = FakeLanguageModel::default(); - let model: Arc = Arc::new(model); - - let language_registry = LanguageRegistry::new(cx.executor()); - let language_registry = Arc::new(language_registry); - - let active_thread = cx.update(|window, cx| { - cx.new(|cx| { - ActiveThread::new( - thread.clone(), - thread_store.clone(), - text_thread_store, - context_store.clone(), - language_registry.clone(), - workspace.downgrade(), - window, - cx, - ) - }) - }); - - (cx, active_thread, workspace, thread, model) - } -} diff --git a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs index 3bd5ed40d2f265287f6fe22dfbc2a19487149c37..cb9aabab9250b2f6175912c0a73242bc7f331dc6 100644 --- a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs +++ b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs @@ -2,7 +2,7 @@ mod profile_modal_header; use std::sync::Arc; -use agent_settings::{AgentProfileId, AgentSettings, builtin_profiles}; +use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles}; use assistant_tool::ToolWorkingSet; use editor::Editor; use fs::Fs; @@ -16,7 +16,6 @@ use workspace::{ModalView, Workspace}; use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader; use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate}; use crate::{AgentPanel, ManageProfiles}; -use agent::agent_profile::AgentProfile; use super::tool_picker::ToolPickerMode; diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 14a60b30148acea49ed81832287a1a8ef51f65a5..5ae43398d626fd5b5208e5e73d2c034644a29d95 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1,7 +1,6 @@ use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll}; use acp_thread::{AcpThread, AcpThreadEvent}; use action_log::ActionLog; -use agent::{Thread, ThreadEvent, ThreadSummary}; use agent_settings::AgentSettings; use anyhow::Result; use buffer_diff::DiffHunkStatus; @@ -19,7 +18,6 @@ use gpui::{ }; use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point}; -use language_model::StopReason; use multi_buffer::PathKey; use project::{Project, ProjectItem, ProjectPath}; use settings::{Settings, SettingsStore}; @@ -51,34 +49,29 @@ pub struct AgentDiffPane { #[derive(PartialEq, Eq, Clone)] pub enum AgentDiffThread { - Native(Entity), AcpThread(Entity), } impl AgentDiffThread { fn project(&self, cx: &App) -> Entity { match self { - AgentDiffThread::Native(thread) => thread.read(cx).project().clone(), AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(), } } fn action_log(&self, cx: &App) -> Entity { match self { - AgentDiffThread::Native(thread) => thread.read(cx).action_log().clone(), AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(), } } - fn summary(&self, cx: &App) -> ThreadSummary { + fn title(&self, cx: &App) -> SharedString { match self { - AgentDiffThread::Native(thread) => thread.read(cx).summary().clone(), - AgentDiffThread::AcpThread(thread) => ThreadSummary::Ready(thread.read(cx).title()), + AgentDiffThread::AcpThread(thread) => thread.read(cx).title(), } } fn is_generating(&self, cx: &App) -> bool { match self { - AgentDiffThread::Native(thread) => thread.read(cx).is_generating(), AgentDiffThread::AcpThread(thread) => { thread.read(cx).status() == acp_thread::ThreadStatus::Generating } @@ -87,14 +80,12 @@ impl AgentDiffThread { fn has_pending_edit_tool_uses(&self, cx: &App) -> bool { match self { - AgentDiffThread::Native(thread) => thread.read(cx).has_pending_edit_tool_uses(), AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(), } } fn downgrade(&self) -> WeakAgentDiffThread { match self { - AgentDiffThread::Native(thread) => WeakAgentDiffThread::Native(thread.downgrade()), AgentDiffThread::AcpThread(thread) => { WeakAgentDiffThread::AcpThread(thread.downgrade()) } @@ -102,12 +93,6 @@ impl AgentDiffThread { } } -impl From> for AgentDiffThread { - fn from(entity: Entity) -> Self { - AgentDiffThread::Native(entity) - } -} - impl From> for AgentDiffThread { fn from(entity: Entity) -> Self { AgentDiffThread::AcpThread(entity) @@ -116,25 +101,17 @@ impl From> for AgentDiffThread { #[derive(PartialEq, Eq, Clone)] pub enum WeakAgentDiffThread { - Native(WeakEntity), AcpThread(WeakEntity), } impl WeakAgentDiffThread { pub fn upgrade(&self) -> Option { match self { - WeakAgentDiffThread::Native(weak) => weak.upgrade().map(AgentDiffThread::Native), WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread), } } } -impl From> for WeakAgentDiffThread { - fn from(entity: WeakEntity) -> Self { - WeakAgentDiffThread::Native(entity) - } -} - impl From> for WeakAgentDiffThread { fn from(entity: WeakEntity) -> Self { WeakAgentDiffThread::AcpThread(entity) @@ -203,10 +180,6 @@ impl AgentDiffPane { this.update_excerpts(window, cx) }), match &thread { - AgentDiffThread::Native(thread) => cx - .subscribe(thread, |this, _thread, event, cx| { - this.handle_native_thread_event(event, cx) - }), AgentDiffThread::AcpThread(thread) => cx .subscribe(thread, |this, _thread, event, cx| { this.handle_acp_thread_event(event, cx) @@ -313,19 +286,13 @@ impl AgentDiffPane { } fn update_title(&mut self, cx: &mut Context) { - let new_title = self.thread.summary(cx).unwrap_or("Agent Changes"); + let new_title = self.thread.title(cx); if new_title != self.title { self.title = new_title; cx.emit(EditorEvent::TitleChanged); } } - fn handle_native_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context) { - if let ThreadEvent::SummaryGenerated = event { - self.update_title(cx) - } - } - fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context) { if let AcpThreadEvent::TitleUpdated = event { self.update_title(cx) @@ -569,8 +536,8 @@ impl Item for AgentDiffPane { } fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { - let summary = self.thread.summary(cx).unwrap_or("Agent Changes"); - Label::new(format!("Review: {}", summary)) + let title = self.thread.title(cx); + Label::new(format!("Review: {}", title)) .color(if params.selected { Color::Default } else { @@ -1339,12 +1306,6 @@ impl AgentDiff { }); let thread_subscription = match &thread { - AgentDiffThread::Native(thread) => cx.subscribe_in(thread, window, { - let workspace = workspace.clone(); - move |this, _thread, event, window, cx| { - this.handle_native_thread_event(&workspace, event, window, cx) - } - }), AgentDiffThread::AcpThread(thread) => cx.subscribe_in(thread, window, { let workspace = workspace.clone(); move |this, thread, event, window, cx| { @@ -1447,47 +1408,6 @@ impl AgentDiff { }); } - fn handle_native_thread_event( - &mut self, - workspace: &WeakEntity, - event: &ThreadEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - ThreadEvent::NewRequest - | ThreadEvent::Stopped(Ok(StopReason::EndTurn)) - | ThreadEvent::Stopped(Ok(StopReason::MaxTokens)) - | ThreadEvent::Stopped(Ok(StopReason::Refusal)) - | ThreadEvent::Stopped(Err(_)) - | ThreadEvent::ShowError(_) - | ThreadEvent::CompletionCanceled => { - self.update_reviewing_editors(workspace, window, cx); - } - // intentionally being exhaustive in case we add a variant we should handle - ThreadEvent::Stopped(Ok(StopReason::ToolUse)) - | ThreadEvent::StreamedCompletion - | ThreadEvent::ReceivedTextChunk - | ThreadEvent::StreamedAssistantText(_, _) - | ThreadEvent::StreamedAssistantThinking(_, _) - | ThreadEvent::StreamedToolUse { .. } - | ThreadEvent::InvalidToolInput { .. } - | ThreadEvent::MissingToolUse { .. } - | ThreadEvent::MessageAdded(_) - | ThreadEvent::MessageEdited(_) - | ThreadEvent::MessageDeleted(_) - | ThreadEvent::SummaryGenerated - | ThreadEvent::SummaryChanged - | ThreadEvent::UsePendingTools { .. } - | ThreadEvent::ToolFinished { .. } - | ThreadEvent::CheckpointChanged - | ThreadEvent::ToolConfirmationNeeded - | ThreadEvent::ToolUseLimitReached - | ThreadEvent::CancelEditing - | ThreadEvent::ProfileChanged => {} - } - } - fn handle_acp_thread_event( &mut self, workspace: &WeakEntity, @@ -1890,16 +1810,14 @@ impl editor::Addon for EditorAgentDiffAddon { mod tests { use super::*; use crate::Keep; - use agent::thread_store::{self, ThreadStore}; + use acp_thread::AgentConnection as _; use agent_settings::AgentSettings; - use assistant_tool::ToolWorkingSet; use editor::EditorSettings; use gpui::{TestAppContext, UpdateGlobal, VisualTestContext}; use project::{FakeFs, Project}; - use prompt_store::PromptBuilder; use serde_json::json; use settings::{Settings, SettingsStore}; - use std::sync::Arc; + use std::{path::Path, rc::Rc}; use theme::ThemeSettings; use util::path; @@ -1912,7 +1830,6 @@ mod tests { Project::init_settings(cx); AgentSettings::register(cx); prompt_store::init(cx); - thread_store::init(cx); workspace::init_settings(cx); ThemeSettings::register(cx); EditorSettings::register(cx); @@ -1932,21 +1849,17 @@ mod tests { }) .unwrap(); - let prompt_store = None; - let thread_store = cx + let connection = Rc::new(acp_thread::StubAgentConnection::new()); + let thread = cx .update(|cx| { - ThreadStore::load( - project.clone(), - cx.new(|_| ToolWorkingSet::default()), - prompt_store, - Arc::new(PromptBuilder::new(None).unwrap()), - cx, - ) + connection + .clone() + .new_thread(project.clone(), Path::new(path!("/test")), cx) }) .await .unwrap(); - let thread = - AgentDiffThread::Native(thread_store.update(cx, |store, cx| store.create_thread(cx))); + + let thread = AgentDiffThread::AcpThread(thread); let action_log = cx.read(|cx| thread.action_log(cx)); let (workspace, cx) = @@ -2069,7 +1982,6 @@ mod tests { Project::init_settings(cx); AgentSettings::register(cx); prompt_store::init(cx); - thread_store::init(cx); workspace::init_settings(cx); ThemeSettings::register(cx); EditorSettings::register(cx); @@ -2098,22 +2010,6 @@ mod tests { }) .unwrap(); - let prompt_store = None; - let thread_store = cx - .update(|cx| { - ThreadStore::load( - project.clone(), - cx.new(|_| ToolWorkingSet::default()), - prompt_store, - Arc::new(PromptBuilder::new(None).unwrap()), - cx, - ) - }) - .await - .unwrap(); - let thread = thread_store.update(cx, |store, cx| store.create_thread(cx)); - let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); @@ -2132,8 +2028,19 @@ mod tests { } }); + let connection = Rc::new(acp_thread::StubAgentConnection::new()); + let thread = cx + .update(|_, cx| { + connection + .clone() + .new_thread(project.clone(), Path::new(path!("/test")), cx) + }) + .await + .unwrap(); + let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); + // Set the active thread - let thread = AgentDiffThread::Native(thread); + let thread = AgentDiffThread::AcpThread(thread); cx.update(|window, cx| { AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx) }); diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs index 3de1027d91f6d613e9f3aa723b345e5d5f17ee6f..58b95d9b1f1b1e8abe7335a2299bee7545b7653e 100644 --- a/crates/agent_ui/src/agent_model_selector.rs +++ b/crates/agent_ui/src/agent_model_selector.rs @@ -5,7 +5,6 @@ use crate::{ use agent_settings::AgentSettings; use fs::Fs; use gpui::{Entity, FocusHandle, SharedString}; -use language_model::{ConfiguredModel, LanguageModelRegistry}; use picker::popover_menu::PickerPopoverMenu; use settings::update_settings_file; use std::sync::Arc; @@ -39,28 +38,6 @@ impl AgentModelSelector { let provider = model.provider_id().0.to_string(); let model_id = model.id().0.to_string(); match &model_usage_context { - ModelUsageContext::Thread(thread) => { - thread.update(cx, |thread, cx| { - let registry = LanguageModelRegistry::read_global(cx); - if let Some(provider) = registry.provider(&model.provider_id()) - { - thread.set_configured_model( - Some(ConfiguredModel { - provider, - model: model.clone(), - }), - cx, - ); - } - }); - update_settings_file::( - fs.clone(), - cx, - move |settings, _cx| { - settings.set_model(model.clone()); - }, - ); - } ModelUsageContext::InlineAssistant => { update_settings_file::( fs.clone(), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 2ace7096c2588699939bc02b92841ea5b5db2e06..7334f1cf1283ad45e0e98d296b3641c9b8812ef5 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2,7 +2,6 @@ use std::ops::{Not, Range}; use std::path::Path; use std::rc::Rc; use std::sync::Arc; -use std::time::Duration; use acp_thread::AcpThread; use agent2::{DbThreadMetadata, HistoryEntry}; @@ -15,64 +14,52 @@ use zed_actions::OpenBrowser; use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent}; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; -use crate::agent_diff::AgentDiffThread; use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal}; use crate::{ - AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, - DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, - NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, - ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, - ToggleNewThreadMenu, ToggleOptionsMenu, + AddContextServer, DeleteRecentlyOpenThread, Follow, InlineAssistant, NewTextThread, NewThread, + OpenActiveThreadAsMarkdown, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, + ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu, acp::AcpThreadView, - active_thread::{self, ActiveThread, ActiveThreadEvent}, agent_configuration::{AgentConfiguration, AssistantConfigurationEvent}, - agent_diff::AgentDiff, - message_editor::{MessageEditor, MessageEditorEvent}, slash_command::SlashCommandCompletionProvider, - text_thread_editor::{ - AgentPanelDelegate, TextThreadEditor, humanize_token_count, make_lsp_adapter_delegate, - }, - thread_history::{HistoryEntryElement, ThreadHistory}, + text_thread_editor::{AgentPanelDelegate, TextThreadEditor, make_lsp_adapter_delegate}, ui::{AgentOnboardingModal, EndTrialUpsell}, }; use crate::{ ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command, }; use agent::{ - Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio, context_store::ContextStore, history_store::{HistoryEntryId, HistoryStore}, thread_store::{TextThreadStore, ThreadStore}, }; -use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView}; +use agent_settings::{AgentDockPosition, AgentSettings, DefaultView}; use ai_onboarding::AgentPanelOnboarding; use anyhow::{Result, anyhow}; use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; use assistant_slash_command::SlashCommandWorkingSet; use assistant_tool::ToolWorkingSet; use client::{UserStore, zed_urls}; -use cloud_llm_client::{CompletionIntent, Plan, PlanV1, PlanV2, UsageLimit}; +use cloud_llm_client::{Plan, PlanV1, PlanV2, UsageLimit}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; -use feature_flags::{self, ClaudeCodeFeatureFlag, FeatureFlagAppExt, GeminiAndNativeFeatureFlag}; use fs::Fs; use gpui::{ - Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem, - Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, - Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, + Action, AnyElement, App, AsyncWindowContext, Corner, DismissEvent, Entity, EventEmitter, + ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, Subscription, Task, UpdateGlobal, + WeakEntity, prelude::*, }; use language::LanguageRegistry; -use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry}; +use language_model::{ConfigurationError, LanguageModelRegistry}; use project::{DisableAiSettings, Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use rules_library::{RulesLibrary, open_rules_library}; use search::{BufferSearchBar, buffer_search}; use settings::{Settings, SettingsStore, update_settings_file}; use theme::ThemeSettings; -use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu, - PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, + Callout, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, + ProgressBar, Tab, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::{ @@ -81,10 +68,7 @@ use workspace::{ }; use zed_actions::{ DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize, - agent::{ - OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding, - ToggleModelSelector, - }, + agent::{OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding}, assistant::{OpenRulesLibrary, ToggleFocus}, }; @@ -150,37 +134,9 @@ pub fn init(cx: &mut App) { }); } }) - .register_action(|workspace, _: &OpenAgentDiff, window, cx| { - if let Some(panel) = workspace.panel::(cx) { - workspace.focus_panel::(window, cx); - match &panel.read(cx).active_view { - ActiveView::Thread { thread, .. } => { - let thread = thread.read(cx).thread().clone(); - AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx); - } - ActiveView::ExternalAgentThread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => {} - } - } - }) .register_action(|workspace, _: &Follow, window, cx| { workspace.follow(CollaboratorId::Agent, window, cx); }) - .register_action(|workspace, _: &ExpandMessageEditor, window, cx| { - let Some(panel) = workspace.panel::(cx) else { - return; - }; - workspace.focus_panel::(window, cx); - panel.update(cx, |panel, cx| { - if let Some(message_editor) = panel.active_message_editor() { - message_editor.update(cx, |editor, cx| { - editor.expand_message_editor(&ExpandMessageEditor, window, cx); - }); - } - }); - }) .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); @@ -230,12 +186,6 @@ pub fn init(cx: &mut App) { } enum ActiveView { - Thread { - thread: Entity, - change_title_editor: Entity, - message_editor: Entity, - _subscriptions: Vec, - }, ExternalAgentThread { thread_view: Entity, }, @@ -305,100 +255,38 @@ impl From for AgentType { impl ActiveView { pub fn which_font_size_used(&self) -> WhichFontSize { match self { - ActiveView::Thread { .. } - | ActiveView::ExternalAgentThread { .. } - | ActiveView::History => WhichFontSize::AgentFont, + ActiveView::ExternalAgentThread { .. } | ActiveView::History => { + WhichFontSize::AgentFont + } ActiveView::TextThread { .. } => WhichFontSize::BufferFont, ActiveView::Configuration => WhichFontSize::None, } } - pub fn thread( - active_thread: Entity, - message_editor: Entity, + pub fn native_agent( + fs: Arc, + prompt_store: Option>, + acp_history_store: Entity, + project: Entity, + workspace: WeakEntity, window: &mut Window, - cx: &mut Context, + cx: &mut App, ) -> Self { - let summary = active_thread.read(cx).summary(cx).or_default(); - - let editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_text(summary.clone(), window, cx); - editor + let thread_view = cx.new(|cx| { + crate::acp::AcpThreadView::new( + ExternalAgent::NativeAgent.server(fs, acp_history_store.clone()), + None, + None, + workspace, + project, + acp_history_store, + prompt_store, + window, + cx, + ) }); - let subscriptions = vec![ - cx.subscribe(&message_editor, |this, _, event, cx| match event { - MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => { - cx.notify(); - } - MessageEditorEvent::ScrollThreadToBottom => match &this.active_view { - ActiveView::Thread { thread, .. } => { - thread.update(cx, |thread, cx| { - thread.scroll_to_bottom(cx); - }); - } - ActiveView::ExternalAgentThread { .. } => {} - ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => {} - }, - }), - window.subscribe(&editor, cx, { - { - let thread = active_thread.clone(); - move |editor, event, window, cx| match event { - EditorEvent::BufferEdited => { - let new_summary = editor.read(cx).text(cx); - - thread.update(cx, |thread, cx| { - thread.thread().update(cx, |thread, cx| { - thread.set_summary(new_summary, cx); - }); - }) - } - EditorEvent::Blurred => { - if editor.read(cx).text(cx).is_empty() { - let summary = thread.read(cx).summary(cx).or_default(); - - editor.update(cx, |editor, cx| { - editor.set_text(summary, window, cx); - }); - } - } - _ => {} - } - } - }), - cx.subscribe(&active_thread, |_, _, event, cx| match &event { - ActiveThreadEvent::EditingMessageTokenCountChanged => { - cx.notify(); - } - }), - cx.subscribe_in(&active_thread.read(cx).thread().clone(), window, { - let editor = editor.clone(); - move |_, thread, event, window, cx| match event { - ThreadEvent::SummaryGenerated => { - let summary = thread.read(cx).summary().or_default(); - - editor.update(cx, |editor, cx| { - editor.set_text(summary, window, cx); - }) - } - ThreadEvent::MessageAdded(_) => { - cx.notify(); - } - _ => {} - } - }), - ]; - - Self::Thread { - change_title_editor: editor, - thread: active_thread, - message_editor, - _subscriptions: subscriptions, - } + Self::ExternalAgentThread { thread_view } } pub fn prompt_editor( @@ -524,18 +412,14 @@ pub struct AgentPanel { thread_store: Entity, acp_history: Entity, acp_history_store: Entity, - _default_model_subscription: Subscription, context_store: Entity, prompt_store: Option>, inline_assist_context_store: Entity, configuration: Option>, configuration_subscription: Option, - local_timezone: UtcOffset, active_view: ActiveView, previous_view: Option, history_store: Entity, - history: Entity, - hovered_recent_history_item: Option, new_thread_menu_handle: PopoverMenuHandle, agent_panel_menu_handle: PopoverMenuHandle, assistant_navigation_menu_handle: PopoverMenuHandle, @@ -655,45 +539,17 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) -> Self { - let thread = thread_store.update(cx, |this, cx| this.create_thread(cx)); let fs = workspace.app_state().fs.clone(); let user_store = workspace.app_state().user_store.clone(); let project = workspace.project(); let language_registry = project.read(cx).languages().clone(); let client = workspace.client().clone(); let workspace = workspace.weak_handle(); - let weak_self = cx.entity().downgrade(); - let message_editor_context_store = - cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade()))); let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade()))); - let thread_id = thread.read(cx).id().clone(); - - let history_store = cx.new(|cx| { - HistoryStore::new( - thread_store.clone(), - context_store.clone(), - [HistoryEntryId::Thread(thread_id)], - cx, - ) - }); - - let message_editor = cx.new(|cx| { - MessageEditor::new( - fs.clone(), - workspace.clone(), - message_editor_context_store.clone(), - prompt_store.clone(), - thread_store.downgrade(), - context_store.downgrade(), - Some(history_store.downgrade()), - thread.clone(), - window, - cx, - ) - }); + let history_store = cx.new(|cx| HistoryStore::new(context_store.clone(), [], cx)); let acp_history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx)); let acp_history = cx.new(|cx| AcpThreadHistory::new(acp_history_store.clone(), window, cx)); @@ -720,22 +576,17 @@ impl AgentPanel { cx.observe(&history_store, |_, _, cx| cx.notify()).detach(); - let active_thread = cx.new(|cx| { - ActiveThread::new( - thread.clone(), - thread_store.clone(), - context_store.clone(), - message_editor_context_store.clone(), - language_registry.clone(), + let panel_type = AgentSettings::get_global(cx).default_view; + let active_view = match panel_type { + DefaultView::Thread => ActiveView::native_agent( + fs.clone(), + prompt_store.clone(), + acp_history_store.clone(), + project.clone(), workspace.clone(), window, cx, - ) - }); - - let panel_type = AgentSettings::get_global(cx).default_view; - let active_view = match panel_type { - DefaultView::Thread => ActiveView::thread(active_thread, message_editor, window, cx), + ), DefaultView::TextThread => { let context = context_store.update(cx, |context_store, cx| context_store.create(cx)); @@ -764,20 +615,14 @@ impl AgentPanel { } }; - AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx); - - let weak_panel = weak_self.clone(); + let weak_panel = cx.entity().downgrade(); window.defer(cx, move |window, cx| { let panel = weak_panel.clone(); let assistant_navigation_menu = ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| { if let Some(panel) = panel.upgrade() { - if cx.has_flag::() { - menu = Self::populate_recently_opened_menu_section_new(menu, panel, cx); - } else { - menu = Self::populate_recently_opened_menu_section_old(menu, panel, cx); - } + menu = Self::populate_recently_opened_menu_section(menu, panel, cx); } menu.action("View All", Box::new(OpenHistory)) .end_slot_action(DeleteRecentlyOpenThread.boxed_clone()) @@ -803,26 +648,6 @@ impl AgentPanel { .ok(); }); - let _default_model_subscription = - cx.subscribe( - &LanguageModelRegistry::global(cx), - |this, _, event: &language_model::Event, cx| { - if let language_model::Event::DefaultModelChanged = event { - match &this.active_view { - ActiveView::Thread { thread, .. } => { - thread.read(cx).thread().clone().update(cx, |thread, cx| { - thread.get_or_init_configured_model(cx) - }); - } - ActiveView::ExternalAgentThread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => {} - } - } - }, - ); - let onboarding = cx.new(|cx| { AgentPanelOnboarding::new( user_store.clone(), @@ -842,20 +667,15 @@ impl AgentPanel { fs: fs.clone(), language_registry, thread_store: thread_store.clone(), - _default_model_subscription, context_store, prompt_store, configuration: None, configuration_subscription: None, - local_timezone: UtcOffset::from_whole_seconds( - chrono::Local::now().offset().local_minus_utc(), - ) - .unwrap(), + inline_assist_context_store, previous_view: None, history_store: history_store.clone(), - history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)), - hovered_recent_history_item: None, + new_thread_menu_handle: PopoverMenuHandle::default(), agent_panel_menu_handle: PopoverMenuHandle::default(), assistant_navigation_menu_handle: PopoverMenuHandle::default(), @@ -886,10 +706,6 @@ impl AgentPanel { } } - pub(crate) fn local_timezone(&self) -> UtcOffset { - self.local_timezone - } - pub(crate) fn prompt_store(&self) -> &Option> { &self.prompt_store } @@ -906,118 +722,15 @@ impl AgentPanel { &self.context_store } - fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context) { - match &self.active_view { - ActiveView::Thread { thread, .. } => { - thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx)); - } - ActiveView::ExternalAgentThread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => {} - } - } - - fn active_message_editor(&self) -> Option<&Entity> { - match &self.active_view { - ActiveView::Thread { message_editor, .. } => Some(message_editor), - ActiveView::ExternalAgentThread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => None, - } - } - fn active_thread_view(&self) -> Option<&Entity> { match &self.active_view { ActiveView::ExternalAgentThread { thread_view, .. } => Some(thread_view), - ActiveView::Thread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => None, + ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, } } - fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context) { - if cx.has_flag::() { - return self.new_agent_thread(AgentType::NativeAgent, window, cx); - } - // Preserve chat box text when using creating new thread - let preserved_text = self - .active_message_editor() - .map(|editor| editor.read(cx).get_text(cx).trim().to_string()); - - let thread = self - .thread_store - .update(cx, |this, cx| this.create_thread(cx)); - - let context_store = cx.new(|_cx| { - ContextStore::new( - self.project.downgrade(), - Some(self.thread_store.downgrade()), - ) - }); - - if let Some(other_thread_id) = action.from_thread_id.clone() { - let other_thread_task = self.thread_store.update(cx, |this, cx| { - this.open_thread(&other_thread_id, window, cx) - }); - - cx.spawn({ - let context_store = context_store.clone(); - - async move |_panel, cx| { - let other_thread = other_thread_task.await?; - - context_store.update(cx, |this, cx| { - this.add_thread(other_thread, false, cx); - })?; - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - } - - let active_thread = cx.new(|cx| { - ActiveThread::new( - thread.clone(), - self.thread_store.clone(), - self.context_store.clone(), - context_store.clone(), - self.language_registry.clone(), - self.workspace.clone(), - window, - cx, - ) - }); - - let message_editor = cx.new(|cx| { - MessageEditor::new( - self.fs.clone(), - self.workspace.clone(), - context_store.clone(), - self.prompt_store.clone(), - self.thread_store.downgrade(), - self.context_store.downgrade(), - Some(self.history_store.downgrade()), - thread.clone(), - window, - cx, - ) - }); - - if let Some(text) = preserved_text { - message_editor.update(cx, |editor, cx| { - editor.set_text(text, window, cx); - }); - } - - message_editor.focus_handle(cx).focus(window); - - let thread_view = ActiveView::thread(active_thread, message_editor, window, cx); - self.set_active_view(thread_view, window, cx); - - AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); + fn new_thread(&mut self, _action: &NewThread, window: &mut Window, cx: &mut Context) { + self.new_agent_thread(AgentType::NativeAgent, window, cx); } fn new_native_agent_thread_from_summary( @@ -1153,21 +866,6 @@ impl AgentPanel { let server = ext_agent.server(fs, history); this.update_in(cx, |this, window, cx| { - match ext_agent { - crate::ExternalAgent::Gemini - | crate::ExternalAgent::NativeAgent - | crate::ExternalAgent::Custom { .. } => { - if !cx.has_flag::() { - return; - } - } - crate::ExternalAgent::ClaudeCode => { - if !cx.has_flag::() { - return; - } - } - } - let selected_agent = ext_agent.into(); if this.selected_agent != selected_agent { this.selected_agent = selected_agent; @@ -1289,72 +987,6 @@ impl AgentPanel { ); } - pub(crate) fn open_thread_by_id( - &mut self, - thread_id: &ThreadId, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let open_thread_task = self - .thread_store - .update(cx, |this, cx| this.open_thread(thread_id, window, cx)); - cx.spawn_in(window, async move |this, cx| { - let thread = open_thread_task.await?; - this.update_in(cx, |this, window, cx| { - this.open_thread(thread, window, cx); - anyhow::Ok(()) - })??; - Ok(()) - }) - } - - pub(crate) fn open_thread( - &mut self, - thread: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let context_store = cx.new(|_cx| { - ContextStore::new( - self.project.downgrade(), - Some(self.thread_store.downgrade()), - ) - }); - - let active_thread = cx.new(|cx| { - ActiveThread::new( - thread.clone(), - self.thread_store.clone(), - self.context_store.clone(), - context_store.clone(), - self.language_registry.clone(), - self.workspace.clone(), - window, - cx, - ) - }); - - let message_editor = cx.new(|cx| { - MessageEditor::new( - self.fs.clone(), - self.workspace.clone(), - context_store, - self.prompt_store.clone(), - self.thread_store.downgrade(), - self.context_store.downgrade(), - Some(self.history_store.downgrade()), - thread.clone(), - window, - cx, - ) - }); - message_editor.focus_handle(cx).focus(window); - - let thread_view = ActiveView::thread(active_thread, message_editor, window, cx); - self.set_active_view(thread_view, window, cx); - AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); - } - pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context) { match self.active_view { ActiveView::Configuration | ActiveView::History => { @@ -1362,9 +994,6 @@ impl AgentPanel { self.active_view = previous_view; match &self.active_view { - ActiveView::Thread { message_editor, .. } => { - message_editor.focus_handle(cx).focus(window); - } ActiveView::ExternalAgentThread { thread_view } => { thread_view.focus_handle(cx).focus(window); } @@ -1479,33 +1108,6 @@ impl AgentPanel { } } - pub fn open_agent_diff( - &mut self, - _: &OpenAgentDiff, - window: &mut Window, - cx: &mut Context, - ) { - match &self.active_view { - ActiveView::Thread { thread, .. } => { - let thread = thread.read(cx).thread().clone(); - self.workspace - .update(cx, |workspace, cx| { - AgentDiffPane::deploy_in_workspace( - AgentDiffThread::Native(thread), - workspace, - window, - cx, - ) - }) - .log_err(); - } - ActiveView::ExternalAgentThread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => {} - } - } - pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context) { let agent_server_store = self.project.read(cx).agent_server_store().clone(); let context_server_store = self.project.read(cx).context_server_store(); @@ -1548,15 +1150,6 @@ impl AgentPanel { }; match &self.active_view { - ActiveView::Thread { thread, .. } => { - active_thread::open_active_thread_as_markdown( - thread.read(cx).thread().clone(), - workspace, - window, - cx, - ) - .detach_and_log_err(cx); - } ActiveView::ExternalAgentThread { thread_view } => { thread_view .update(cx, |thread_view, cx| { @@ -1590,29 +1183,18 @@ impl AgentPanel { } self.new_thread(&NewThread::default(), window, cx); - if let Some((thread, model)) = - self.active_thread(cx).zip(provider.default_model(cx)) + if let Some((thread, model)) = self + .active_native_agent_thread(cx) + .zip(provider.default_model(cx)) { thread.update(cx, |thread, cx| { - thread.set_configured_model( - Some(ConfiguredModel { - provider: provider.clone(), - model, - }), - cx, - ); + thread.set_model(model, cx); }); } } } } - pub(crate) fn active_thread(&self, cx: &App) -> Option> { - match &self.active_view { - ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), - _ => None, - } - } pub(crate) fn active_agent_thread(&self, cx: &App) -> Option> { match &self.active_view { ActiveView::ExternalAgentThread { thread_view, .. } => { @@ -1622,90 +1204,30 @@ impl AgentPanel { } } - pub(crate) fn delete_thread( - &mut self, - thread_id: &ThreadId, - cx: &mut Context, - ) -> Task> { - self.thread_store - .update(cx, |this, cx| this.delete_thread(thread_id, cx)) - } - - fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context) { - let ActiveView::Thread { thread, .. } = &self.active_view else { - return; - }; - - let thread_state = thread.read(cx).thread().read(cx); - if !thread_state.tool_use_limit_reached() { - return; + pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option> { + match &self.active_view { + ActiveView::ExternalAgentThread { thread_view, .. } => { + thread_view.read(cx).as_native_thread(cx) + } + _ => None, } + } - let model = thread_state.configured_model().map(|cm| cm.model); - if let Some(model) = model { - thread.update(cx, |active_thread, cx| { - active_thread.thread().update(cx, |thread, cx| { - thread.insert_invisible_continue_message(cx); - thread.advance_prompt_id(); - thread.send_to_model( - model, - CompletionIntent::UserPrompt, - Some(window.window_handle()), - cx, - ); - }); - }); - } else { - log::warn!("No configured model available for continuation"); + pub(crate) fn active_context_editor(&self) -> Option> { + match &self.active_view { + ActiveView::TextThread { context_editor, .. } => Some(context_editor.clone()), + _ => None, } } - fn toggle_burn_mode( + fn set_active_view( &mut self, - _: &ToggleBurnMode, - _window: &mut Window, + new_view: ActiveView, + window: &mut Window, cx: &mut Context, ) { - let ActiveView::Thread { thread, .. } = &self.active_view else { - return; - }; - - thread.update(cx, |active_thread, cx| { - active_thread.thread().update(cx, |thread, _cx| { - let current_mode = thread.completion_mode(); - - thread.set_completion_mode(match current_mode { - CompletionMode::Burn => CompletionMode::Normal, - CompletionMode::Normal => CompletionMode::Burn, - }); - }); - }); - } - - pub(crate) fn active_context_editor(&self) -> Option> { - match &self.active_view { - ActiveView::TextThread { context_editor, .. } => Some(context_editor.clone()), - _ => None, - } - } - - pub(crate) fn delete_context( - &mut self, - path: Arc, - cx: &mut Context, - ) -> Task> { - self.context_store - .update(cx, |this, cx| this.delete_local_context(path, cx)) - } - - fn set_active_view( - &mut self, - new_view: ActiveView, - window: &mut Window, - cx: &mut Context, - ) { - let current_is_history = matches!(self.active_view, ActiveView::History); - let new_is_history = matches!(new_view, ActiveView::History); + let current_is_history = matches!(self.active_view, ActiveView::History); + let new_is_history = matches!(new_view, ActiveView::History); let current_is_config = matches!(self.active_view, ActiveView::Configuration); let new_is_config = matches!(new_view, ActiveView::Configuration); @@ -1713,21 +1235,7 @@ impl AgentPanel { let current_is_special = current_is_history || current_is_config; let new_is_special = new_is_history || new_is_config; - if let ActiveView::Thread { thread, .. } = &self.active_view { - let thread = thread.read(cx); - if thread.is_empty() { - let id = thread.thread().read(cx).id().clone(); - self.history_store.update(cx, |store, cx| { - store.remove_recently_opened_thread(id, cx); - }); - } - } - match &new_view { - ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| { - let id = thread.read(cx).thread().read(cx).id().clone(); - store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx); - }), ActiveView::TextThread { context_editor, .. } => { self.history_store.update(cx, |store, cx| { if let Some(path) = context_editor.read(cx).context().read(cx).path() { @@ -1761,71 +1269,7 @@ impl AgentPanel { self.focus_handle(cx).focus(window); } - fn populate_recently_opened_menu_section_old( - mut menu: ContextMenu, - panel: Entity, - cx: &mut Context, - ) -> ContextMenu { - let entries = panel - .read(cx) - .history_store - .read(cx) - .recently_opened_entries(cx); - - if entries.is_empty() { - return menu; - } - - menu = menu.header("Recently Opened"); - - for entry in entries { - let title = entry.title().clone(); - let id = entry.id(); - - menu = menu.entry_with_end_slot_on_hover( - title, - None, - { - let panel = panel.downgrade(); - let id = id.clone(); - move |window, cx| { - let id = id.clone(); - panel - .update(cx, move |this, cx| match id { - HistoryEntryId::Thread(id) => this - .open_thread_by_id(&id, window, cx) - .detach_and_log_err(cx), - HistoryEntryId::Context(path) => this - .open_saved_prompt_editor(path, window, cx) - .detach_and_log_err(cx), - }) - .ok(); - } - }, - IconName::Close, - "Close Entry".into(), - { - let panel = panel.downgrade(); - let id = id.clone(); - move |_window, cx| { - panel - .update(cx, |this, cx| { - this.history_store.update(cx, |history_store, cx| { - history_store.remove_recently_opened_entry(&id, cx); - }); - }) - .ok(); - } - }, - ); - } - - menu = menu.separator(); - - menu - } - - fn populate_recently_opened_menu_section_new( + fn populate_recently_opened_menu_section( mut menu: ContextMenu, panel: Entity, cx: &mut Context, @@ -1965,15 +1409,8 @@ impl AgentPanel { impl Focusable for AgentPanel { fn focus_handle(&self, cx: &App) -> FocusHandle { match &self.active_view { - ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx), ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx), - ActiveView::History => { - if cx.has_flag::() { - self.acp_history.focus_handle(cx) - } else { - self.history.focus_handle(cx) - } - } + ActiveView::History => self.acp_history.focus_handle(cx), ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx), ActiveView::Configuration => { if let Some(configuration) = self.configuration.as_ref() { @@ -2080,58 +1517,6 @@ impl AgentPanel { const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…"; let content = match &self.active_view { - ActiveView::Thread { - thread: active_thread, - change_title_editor, - .. - } => { - let state = { - let active_thread = active_thread.read(cx); - if active_thread.is_empty() { - &ThreadSummary::Pending - } else { - active_thread.summary(cx) - } - }; - - match state { - ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT) - .truncate() - .color(Color::Muted) - .into_any_element(), - ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER) - .truncate() - .color(Color::Muted) - .into_any_element(), - ThreadSummary::Ready(_) => div() - .w_full() - .child(change_title_editor.clone()) - .into_any_element(), - ThreadSummary::Error => h_flex() - .w_full() - .child(change_title_editor.clone()) - .child( - IconButton::new("retry-summary-generation", IconName::RotateCcw) - .icon_size(IconSize::Small) - .on_click({ - let active_thread = active_thread.clone(); - move |_, _window, cx| { - active_thread.update(cx, |thread, cx| { - thread.regenerate_summary(cx); - }); - } - }) - .tooltip(move |_window, cx| { - cx.new(|_| { - Tooltip::new("Failed to generate title") - .meta("Click to try again") - }) - .into() - }), - ) - .into_any_element(), - } - } ActiveView::ExternalAgentThread { thread_view } => { if let Some(title_editor) = thread_view.read(cx).title_editor() { div() @@ -2392,123 +1777,7 @@ impl AgentPanel { }) } - fn render_toolbar_old(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let focus_handle = self.focus_handle(cx); - - let active_thread = match &self.active_view { - ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), - ActiveView::ExternalAgentThread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => None, - }; - - let new_thread_menu = PopoverMenu::new("new_thread_menu") - .trigger_with_tooltip( - IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), - Tooltip::text("New Thread…"), - ) - .anchor(Corner::TopRight) - .with_handle(self.new_thread_menu_handle.clone()) - .menu({ - move |window, cx| { - let active_thread = active_thread.clone(); - Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { - menu = menu - .context(focus_handle.clone()) - .when_some(active_thread, |this, active_thread| { - let thread = active_thread.read(cx); - - if !thread.is_empty() { - let thread_id = thread.id().clone(); - this.item( - ContextMenuEntry::new("New From Summary") - .icon(IconName::ThreadFromSummary) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - Box::new(NewThread { - from_thread_id: Some(thread_id.clone()), - }), - cx, - ); - }), - ) - } else { - this - } - }) - .item( - ContextMenuEntry::new("New Thread") - .icon(IconName::Thread) - .icon_color(Color::Muted) - .action(NewThread::default().boxed_clone()) - .handler(move |window, cx| { - window.dispatch_action( - NewThread::default().boxed_clone(), - cx, - ); - }), - ) - .item( - ContextMenuEntry::new("New Text Thread") - .icon(IconName::TextThread) - .icon_color(Color::Muted) - .action(NewTextThread.boxed_clone()) - .handler(move |window, cx| { - window.dispatch_action(NewTextThread.boxed_clone(), cx); - }), - ); - menu - })) - } - }); - - h_flex() - .id("assistant-toolbar") - .h(Tab::container_height(cx)) - .max_w_full() - .flex_none() - .justify_between() - .gap_2() - .bg(cx.theme().colors().tab_bar_background) - .border_b_1() - .border_color(cx.theme().colors().border) - .child( - h_flex() - .size_full() - .pl_1() - .gap_1() - .child(match &self.active_view { - ActiveView::History | ActiveView::Configuration => div() - .pl(DynamicSpacing::Base04.rems(cx)) - .child(self.render_toolbar_back_button(cx)) - .into_any_element(), - _ => self - .render_recent_entries_menu(IconName::MenuAlt, Corner::TopLeft, cx) - .into_any_element(), - }) - .child(self.render_title_view(window, cx)), - ) - .child( - h_flex() - .h_full() - .gap_2() - .children(self.render_token_count(cx)) - .child( - h_flex() - .h_full() - .gap(DynamicSpacing::Base02.rems(cx)) - .px(DynamicSpacing::Base08.rems(cx)) - .border_l_1() - .border_color(cx.theme().colors().border) - .child(new_thread_menu) - .child(self.render_panel_options_menu(window, cx)), - ), - ) - } - - fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let agent_server_store = self.project.read(cx).agent_server_store().clone(); let focus_handle = self.focus_handle(cx); @@ -2516,10 +1785,7 @@ impl AgentPanel { ActiveView::ExternalAgentThread { thread_view } => { thread_view.read(cx).as_native_thread(cx) } - ActiveView::Thread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => None, + ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, }; let new_thread_menu = PopoverMenu::new("new_thread_menu") @@ -2552,8 +1818,8 @@ impl AgentPanel { telemetry::event!("New Thread Clicked"); let active_thread = active_thread.clone(); - Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { - menu = menu + Some(ContextMenu::build(window, cx, |menu, _window, cx| { + menu .context(focus_handle.clone()) .header("Zed Agent") .when_some(active_thread, |this, active_thread| { @@ -2632,63 +1898,59 @@ impl AgentPanel { ) .separator() .header("External Agents") - .when(cx.has_flag::(), |menu| { - menu.item( - ContextMenuEntry::new("New Gemini CLI Thread") - .icon(IconName::AiGemini) - .icon_color(Color::Muted) - .disabled(is_via_collab) - .handler({ - let workspace = workspace.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::(cx) - { - panel.update(cx, |panel, cx| { - panel.new_agent_thread( - AgentType::Gemini, - window, - cx, - ); - }); - } - }); - } + .item( + ContextMenuEntry::new("New Gemini CLI Thread") + .icon(IconName::AiGemini) + .icon_color(Color::Muted) + .disabled(is_via_collab) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.new_agent_thread( + AgentType::Gemini, + window, + cx, + ); + }); + } + }); } - }), - ) - }) - .when(cx.has_flag::(), |menu| { - menu.item( - ContextMenuEntry::new("New Claude Code Thread") - .icon(IconName::AiClaude) - .disabled(is_via_collab) - .icon_color(Color::Muted) - .handler({ - let workspace = workspace.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::(cx) - { - panel.update(cx, |panel, cx| { - panel.new_agent_thread( - AgentType::ClaudeCode, - window, - cx, - ); - }); - } - }); - } + } + }), + ) + .item( + ContextMenuEntry::new("New Claude Code Thread") + .icon(IconName::AiClaude) + .disabled(is_via_collab) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.new_agent_thread( + AgentType::ClaudeCode, + window, + cx, + ); + }); + } + }); } - }), - ) - }) - .when(cx.has_flag::(), |mut menu| { + } + }), + ) + .map(|mut menu| { let agent_names = agent_server_store .read(cx) .external_agents() @@ -2739,16 +2001,13 @@ impl AgentPanel { menu }) - .when(cx.has_flag::(), |menu| { - menu.separator().link( + .separator().link( "Add Other Agents", OpenBrowser { url: zed_urls::external_agents_docs(cx), } .boxed_clone(), ) - }); - menu })) } }); @@ -2810,153 +2069,12 @@ impl AgentPanel { ) } - fn render_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if cx.has_flag::() - || cx.has_flag::() - { - self.render_toolbar_new(window, cx).into_any_element() - } else { - self.render_toolbar_old(window, cx).into_any_element() - } - } - - fn render_token_count(&self, cx: &App) -> Option { - match &self.active_view { - ActiveView::Thread { - thread, - message_editor, - .. - } => { - let active_thread = thread.read(cx); - let message_editor = message_editor.read(cx); - - let editor_empty = message_editor.is_editor_fully_empty(cx); - - if active_thread.is_empty() && editor_empty { - return None; - } - - let thread = active_thread.thread().read(cx); - let is_generating = thread.is_generating(); - let conversation_token_usage = thread.total_token_usage()?; - - let (total_token_usage, is_estimating) = - if let Some((editing_message_id, unsent_tokens)) = - active_thread.editing_message_id() - { - let combined = thread - .token_usage_up_to_message(editing_message_id) - .add(unsent_tokens); - - (combined, unsent_tokens > 0) - } else { - let unsent_tokens = - message_editor.last_estimated_token_count().unwrap_or(0); - let combined = conversation_token_usage.add(unsent_tokens); - - (combined, unsent_tokens > 0) - }; - - let is_waiting_to_update_token_count = - message_editor.is_waiting_to_update_token_count(); - - if total_token_usage.total == 0 { - return None; - } - - let token_color = match total_token_usage.ratio() { - TokenUsageRatio::Normal if is_estimating => Color::Default, - TokenUsageRatio::Normal => Color::Muted, - TokenUsageRatio::Warning => Color::Warning, - TokenUsageRatio::Exceeded => Color::Error, - }; - - let token_count = h_flex() - .id("token-count") - .flex_shrink_0() - .gap_0p5() - .when(!is_generating && is_estimating, |parent| { - parent - .child( - h_flex() - .mr_1() - .size_2p5() - .justify_center() - .rounded_full() - .bg(cx.theme().colors().text.opacity(0.1)) - .child( - div().size_1().rounded_full().bg(cx.theme().colors().text), - ), - ) - .tooltip(move |window, cx| { - Tooltip::with_meta( - "Estimated New Token Count", - None, - format!( - "Current Conversation Tokens: {}", - humanize_token_count(conversation_token_usage.total) - ), - window, - cx, - ) - }) - }) - .child( - Label::new(humanize_token_count(total_token_usage.total)) - .size(LabelSize::Small) - .color(token_color) - .map(|label| { - if is_generating || is_waiting_to_update_token_count { - label - .with_animation( - "used-tokens-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.6, 1.)), - |label, delta| label.alpha(delta), - ) - .into_any() - } else { - label.into_any_element() - } - }), - ) - .child(Label::new("/").size(LabelSize::Small).color(Color::Muted)) - .child( - Label::new(humanize_token_count(total_token_usage.max)) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any(); - - Some(token_count) - } - ActiveView::ExternalAgentThread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => None, - } - } - fn should_render_trial_end_upsell(&self, cx: &mut Context) -> bool { if TrialEndUpsell::dismissed() { return false; } match &self.active_view { - ActiveView::Thread { thread, .. } => { - if thread - .read(cx) - .thread() - .read(cx) - .configured_model() - .is_some_and(|model| { - model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID - }) - { - return false; - } - } ActiveView::TextThread { .. } => { if LanguageModelRegistry::global(cx) .read(cx) @@ -3009,15 +2127,10 @@ impl AgentPanel { false } _ => { - let history_is_empty = if cx.has_flag::() { - self.acp_history_store.read(cx).is_empty(cx) - && self - .history_store - .update(cx, |store, cx| store.recent_entries(1, cx).is_empty()) - } else { - self.history_store - .update(cx, |store, cx| store.recent_entries(1, cx).is_empty()) - }; + let history_is_empty = self.acp_history_store.read(cx).is_empty(cx) + && self + .history_store + .update(cx, |store, cx| store.recent_entries(1, cx).is_empty()); let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx) .providers() @@ -3041,14 +2154,10 @@ impl AgentPanel { return None; } - let thread_view = matches!(&self.active_view, ActiveView::Thread { .. }); let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. }); Some( div() - .when(thread_view, |this| { - this.size_full().bg(cx.theme().colors().panel_background) - }) .when(text_thread_view, |this| { this.bg(cx.theme().colors().editor_background) }) @@ -3056,16 +2165,6 @@ impl AgentPanel { ) } - fn render_backdrop(&self, cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .absolute() - .inset_0() - .bg(cx.theme().colors().panel_background) - .opacity(0.8) - .block_mouse_except_scroll() - } - fn render_trial_end_upsell( &self, _window: &mut Window, @@ -3100,198 +2199,6 @@ impl AgentPanel { ) } - fn render_empty_state_section_header( - &self, - label: impl Into, - action_slot: Option, - cx: &mut Context, - ) -> impl IntoElement { - div().pl_1().pr_1p5().child( - h_flex() - .mt_2() - .pl_1p5() - .pb_1() - .w_full() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child( - Label::new(label.into()) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .children(action_slot), - ) - } - - fn render_thread_empty_state( - &self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let recent_history = self - .history_store - .update(cx, |this, cx| this.recent_entries(6, cx)); - - let model_registry = LanguageModelRegistry::read_global(cx); - - let configuration_error = - model_registry.configuration_error(model_registry.default_model(), cx); - - let no_error = configuration_error.is_none(); - let focus_handle = self.focus_handle(cx); - - v_flex() - .size_full() - .bg(cx.theme().colors().panel_background) - .when(recent_history.is_empty(), |this| { - this.child( - v_flex() - .size_full() - .mx_auto() - .justify_center() - .items_center() - .gap_1() - .child(h_flex().child(Headline::new("Welcome to the Agent Panel"))) - .when(no_error, |parent| { - parent - .child(h_flex().child( - Label::new("Ask and build anything.").color(Color::Muted), - )) - .child( - v_flex() - .mt_2() - .gap_1() - .max_w_48() - .child( - Button::new("context", "Add Context") - .label_size(LabelSize::Small) - .icon(IconName::FileCode) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .full_width() - .key_binding(KeyBinding::for_action_in( - &ToggleContextPicker, - &focus_handle, - window, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action( - ToggleContextPicker.boxed_clone(), - cx, - ) - }), - ) - .child( - Button::new("mode", "Switch Model") - .label_size(LabelSize::Small) - .icon(IconName::DatabaseZap) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .full_width() - .key_binding(KeyBinding::for_action_in( - &ToggleModelSelector, - &focus_handle, - window, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action( - ToggleModelSelector.boxed_clone(), - cx, - ) - }), - ) - .child( - Button::new("settings", "View Settings") - .label_size(LabelSize::Small) - .icon(IconName::Settings) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .full_width() - .key_binding(KeyBinding::for_action_in( - &OpenSettings, - &focus_handle, - window, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action( - OpenSettings.boxed_clone(), - cx, - ) - }), - ), - ) - }), - ) - }) - .when(!recent_history.is_empty(), |parent| { - parent - .overflow_hidden() - .justify_end() - .gap_1() - .child( - self.render_empty_state_section_header( - "Recent", - Some( - Button::new("view-history", "View All") - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &OpenHistory, - &self.focus_handle(cx), - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(move |_event, window, cx| { - window.dispatch_action(OpenHistory.boxed_clone(), cx); - }) - .into_any_element(), - ), - cx, - ), - ) - .child( - v_flex().p_1().pr_1p5().gap_1().children( - recent_history - .into_iter() - .enumerate() - .map(|(index, entry)| { - // TODO: Add keyboard navigation. - let is_hovered = - self.hovered_recent_history_item == Some(index); - HistoryEntryElement::new(entry, cx.entity().downgrade()) - .hovered(is_hovered) - .on_hover(cx.listener( - move |this, is_hovered, _window, cx| { - if *is_hovered { - this.hovered_recent_history_item = Some(index); - } else if this.hovered_recent_history_item - == Some(index) - { - this.hovered_recent_history_item = None; - } - cx.notify(); - }, - )) - .into_any_element() - }), - ), - ) - }) - .when_some(configuration_error.as_ref(), |this, err| { - this.child(self.render_configuration_error(false, err, &focus_handle, window, cx)) - }) - } - fn render_configuration_error( &self, border_bottom: bool, @@ -3362,285 +2269,6 @@ impl AgentPanel { } } - fn render_tool_use_limit_reached( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let active_thread = match &self.active_view { - ActiveView::Thread { thread, .. } => thread, - ActiveView::ExternalAgentThread { .. } => { - return None; - } - ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => { - return None; - } - }; - - let thread = active_thread.read(cx).thread().read(cx); - - let tool_use_limit_reached = thread.tool_use_limit_reached(); - if !tool_use_limit_reached { - return None; - } - - let model = thread.configured_model()?.model; - - let focus_handle = self.focus_handle(cx); - - let banner = Banner::new() - .severity(Severity::Info) - .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small)) - .action_slot( - h_flex() - .gap_1() - .child( - Button::new("continue-conversation", "Continue") - .layer(ElevationIndex::ModalSurface) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &ContinueThread, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .on_click(cx.listener(|this, _, window, cx| { - this.continue_conversation(window, cx); - })), - ) - .when(model.supports_burn_mode(), |this| { - this.child( - Button::new("continue-burn-mode", "Continue with Burn Mode") - .style(ButtonStyle::Filled) - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .layer(ElevationIndex::ModalSurface) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &ContinueWithBurnMode, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use.")) - .on_click({ - let active_thread = active_thread.clone(); - cx.listener(move |this, _, window, cx| { - active_thread.update(cx, |active_thread, cx| { - active_thread.thread().update(cx, |thread, _cx| { - thread.set_completion_mode(CompletionMode::Burn); - }); - }); - this.continue_conversation(window, cx); - }) - }), - ) - }), - ); - - Some(div().px_2().pb_2().child(banner).into_any_element()) - } - - fn create_copy_button(&self, message: impl Into) -> impl IntoElement { - let message = message.into(); - - IconButton::new("copy", IconName::Copy) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(Tooltip::text("Copy Error Message")) - .on_click(move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(message.clone())) - }) - } - - fn dismiss_error_button( - &self, - thread: &Entity, - cx: &mut Context, - ) -> impl IntoElement { - IconButton::new("dismiss", IconName::Close) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(Tooltip::text("Dismiss Error")) - .on_click(cx.listener({ - let thread = thread.clone(); - move |_, _, _, cx| { - thread.update(cx, |this, _cx| { - this.clear_last_error(); - }); - - cx.notify(); - } - })) - } - - fn upgrade_button( - &self, - thread: &Entity, - cx: &mut Context, - ) -> impl IntoElement { - Button::new("upgrade", "Upgrade") - .label_size(LabelSize::Small) - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(cx.listener({ - let thread = thread.clone(); - move |_, _, _, cx| { - thread.update(cx, |this, _cx| { - this.clear_last_error(); - }); - - cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)); - cx.notify(); - } - })) - } - - fn render_payment_required_error( - &self, - thread: &Entity, - cx: &mut Context, - ) -> AnyElement { - const ERROR_MESSAGE: &str = - "You reached your free usage limit. Upgrade to Zed Pro for more prompts."; - - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircle) - .title("Free Usage Exceeded") - .description(ERROR_MESSAGE) - .actions_slot( - h_flex() - .gap_0p5() - .child(self.upgrade_button(thread, cx)) - .child(self.create_copy_button(ERROR_MESSAGE)), - ) - .dismiss_action(self.dismiss_error_button(thread, cx)) - .into_any_element() - } - - fn render_model_request_limit_reached_error( - &self, - plan: Plan, - thread: &Entity, - cx: &mut Context, - ) -> AnyElement { - let error_message = match plan { - Plan::V1(PlanV1::ZedPro) => "Upgrade to usage-based billing for more prompts.", - Plan::V1(PlanV1::ZedProTrial) | Plan::V1(PlanV1::ZedFree) => { - "Upgrade to Zed Pro for more prompts." - } - Plan::V2(_) => "", - }; - - Callout::new() - .severity(Severity::Error) - .title("Model Prompt Limit Reached") - .description(error_message) - .actions_slot( - h_flex() - .gap_0p5() - .child(self.upgrade_button(thread, cx)) - .child(self.create_copy_button(error_message)), - ) - .dismiss_action(self.dismiss_error_button(thread, cx)) - .into_any_element() - } - - fn render_retry_button(&self, thread: &Entity) -> AnyElement { - Button::new("retry", "Retry") - .icon(IconName::RotateCw) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .label_size(LabelSize::Small) - .on_click({ - let thread = thread.clone(); - move |_, window, cx| { - thread.update(cx, |thread, cx| { - thread.clear_last_error(); - thread.thread().update(cx, |thread, cx| { - thread.retry_last_completion(Some(window.window_handle()), cx); - }); - }); - } - }) - .into_any_element() - } - - fn render_error_message( - &self, - header: SharedString, - message: SharedString, - thread: &Entity, - cx: &mut Context, - ) -> AnyElement { - let message_with_header = format!("{}\n{}", header, message); - - // Don't show Retry button for refusals - let is_refusal = header == "Request Refused"; - let retry_button = self.render_retry_button(thread); - let copy_button = self.create_copy_button(message_with_header); - - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircle) - .title(header) - .description(message) - .actions_slot( - h_flex() - .gap_0p5() - .when(!is_refusal, |this| this.child(retry_button)) - .child(copy_button), - ) - .dismiss_action(self.dismiss_error_button(thread, cx)) - .into_any_element() - } - - fn render_retryable_error( - &self, - message: SharedString, - can_enable_burn_mode: bool, - thread: &Entity, - ) -> AnyElement { - Callout::new() - .severity(Severity::Error) - .title("Error") - .description(message) - .actions_slot( - h_flex() - .gap_0p5() - .when(can_enable_burn_mode, |this| { - this.child( - Button::new("enable_burn_retry", "Enable Burn Mode and Retry") - .icon(IconName::ZedBurnMode) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .label_size(LabelSize::Small) - .on_click({ - let thread = thread.clone(); - move |_, window, cx| { - thread.update(cx, |thread, cx| { - thread.clear_last_error(); - thread.thread().update(cx, |thread, cx| { - thread.enable_burn_mode_and_retry( - Some(window.window_handle()), - cx, - ); - }); - }); - } - }), - ) - }) - .child(self.render_retry_button(thread)), - ) - .into_any_element() - } - fn render_prompt_editor( &self, context_editor: &Entity, @@ -3748,26 +2376,6 @@ impl AgentPanel { cx: &mut Context, ) { match &self.active_view { - ActiveView::Thread { thread, .. } => { - let context_store = thread.read(cx).context_store().clone(); - context_store.update(cx, move |context_store, cx| { - let mut tasks = Vec::new(); - for project_path in &paths { - tasks.push(context_store.add_file_from_path( - project_path.clone(), - false, - cx, - )); - } - cx.background_spawn(async move { - futures::future::join_all(tasks).await; - // Need to hold onto the worktrees until they have already been used when - // opening the buffers. - drop(added_worktrees); - }) - .detach(); - }); - } ActiveView::ExternalAgentThread { thread_view } => { thread_view.update(cx, |thread_view, cx| { thread_view.insert_dragged_files(paths, added_worktrees, window, cx); @@ -3794,7 +2402,7 @@ impl AgentPanel { match &self.active_view { ActiveView::ExternalAgentThread { .. } => key_context.add("external_agent_thread"), ActiveView::TextThread { .. } => key_context.add("prompt_editor"), - ActiveView::Thread { .. } | ActiveView::History | ActiveView::Configuration => {} + ActiveView::History | ActiveView::Configuration => {} } key_context } @@ -3816,7 +2424,6 @@ impl Render for AgentPanel { .size_full() .justify_between() .key_context(self.key_context()) - .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(|this, action: &NewThread, window, cx| { this.new_thread(action, window, cx); })) @@ -3828,7 +2435,6 @@ impl Render for AgentPanel { })) .on_action(cx.listener(Self::open_active_thread_as_markdown)) .on_action(cx.listener(Self::deploy_rules_library)) - .on_action(cx.listener(Self::open_agent_diff)) .on_action(cx.listener(Self::go_back)) .on_action(cx.listener(Self::toggle_navigation_menu)) .on_action(cx.listener(Self::toggle_options_menu)) @@ -3836,26 +2442,6 @@ impl Render for AgentPanel { .on_action(cx.listener(Self::decrease_font_size)) .on_action(cx.listener(Self::reset_font_size)) .on_action(cx.listener(Self::toggle_zoom)) - .on_action(cx.listener(|this, _: &ContinueThread, window, cx| { - this.continue_conversation(window, cx); - })) - .on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| { - match &this.active_view { - ActiveView::Thread { thread, .. } => { - thread.update(cx, |active_thread, cx| { - active_thread.thread().update(cx, |thread, _cx| { - thread.set_completion_mode(CompletionMode::Burn); - }); - }); - this.continue_conversation(window, cx); - } - ActiveView::ExternalAgentThread { .. } => {} - ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => {} - } - })) - .on_action(cx.listener(Self::toggle_burn_mode)) .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| { if let Some(thread_view) = this.active_thread_view() { thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx)) @@ -3864,59 +2450,10 @@ impl Render for AgentPanel { .child(self.render_toolbar(window, cx)) .children(self.render_onboarding(window, cx)) .map(|parent| match &self.active_view { - ActiveView::Thread { - thread, - message_editor, - .. - } => parent - .child( - if thread.read(cx).is_empty() && !self.should_render_onboarding(cx) { - self.render_thread_empty_state(window, cx) - .into_any_element() - } else { - thread.clone().into_any_element() - }, - ) - .children(self.render_tool_use_limit_reached(window, cx)) - .when_some(thread.read(cx).last_error(), |this, last_error| { - this.child( - div() - .child(match last_error { - ThreadError::PaymentRequired => { - self.render_payment_required_error(thread, cx) - } - ThreadError::ModelRequestLimitReached { plan } => self - .render_model_request_limit_reached_error(plan, thread, cx), - ThreadError::Message { header, message } => { - self.render_error_message(header, message, thread, cx) - } - ThreadError::RetryableError { - message, - can_enable_burn_mode, - } => self.render_retryable_error( - message, - can_enable_burn_mode, - thread, - ), - }) - .into_any(), - ) - }) - .child(h_flex().relative().child(message_editor.clone()).when( - !LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx), - |this| this.child(self.render_backdrop(cx)), - )) - .child(self.render_drag_target(cx)), ActiveView::ExternalAgentThread { thread_view, .. } => parent .child(thread_view.clone()) .child(self.render_drag_target(cx)), - ActiveView::History => { - if cx.has_flag::() { - parent.child(self.acp_history.clone()) - } else { - parent.child(self.history.clone()) - } - } + ActiveView::History => parent.child(self.acp_history.clone()), ActiveView::TextThread { context_editor, buffer_search_bar, @@ -4082,29 +2619,6 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { thread_view.update(cx, |thread_view, cx| { thread_view.insert_selections(window, cx); }); - } else if let Some(message_editor) = panel.active_message_editor() { - message_editor.update(cx, |message_editor, cx| { - message_editor.context_store().update(cx, |store, cx| { - let buffer = buffer.read(cx); - let selection_ranges = selection_ranges - .into_iter() - .flat_map(|range| { - let (start_buffer, start) = - buffer.text_anchor_for_position(range.start, cx)?; - let (end_buffer, end) = - buffer.text_anchor_for_position(range.end, cx)?; - if start_buffer != end_buffer { - return None; - } - Some((start_buffer, start..end)) - }) - .collect::>(); - - for (buffer, range) in selection_ranges { - store.add_selection(buffer, range, cx); - } - }) - }) } else if let Some(context_editor) = panel.active_context_editor() { let snapshot = buffer.read(cx).snapshot(cx); let selection_ranges = selection_ranges diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 09d2179fc3a2ec4ff4288da4062365c51ad4444f..94efa767c5f1cd126695b8345230fba78583eb2f 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -1,5 +1,4 @@ mod acp; -mod active_thread; mod agent_configuration; mod agent_diff; mod agent_model_selector; @@ -8,7 +7,6 @@ mod buffer_codegen; mod context_picker; mod context_server_configuration; mod context_strip; -mod debug; mod inline_assistant; mod inline_prompt_editor; mod language_model_selector; @@ -20,14 +18,12 @@ mod slash_command_settings; mod terminal_codegen; mod terminal_inline_assistant; mod text_thread_editor; -mod thread_history; -mod tool_compatibility; mod ui; use std::rc::Rc; use std::sync::Arc; -use agent::{Thread, ThreadId}; +use agent::ThreadId; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; use assistant_slash_command::SlashCommandRegistry; use client::Client; @@ -47,14 +43,12 @@ use serde::{Deserialize, Serialize}; use settings::{Settings as _, SettingsStore}; use std::any::TypeId; -pub use crate::active_thread::ActiveThread; use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal}; pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate}; pub use crate::inline_assistant::InlineAssistant; use crate::slash_command_settings::SlashCommandSettings; pub use agent_diff::{AgentDiffPane, AgentDiffToolbar}; pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor}; -pub use ui::preview::{all_agent_previews, get_agent_preview}; use zed_actions; actions!( @@ -235,14 +229,12 @@ impl ManageProfiles { #[derive(Clone)] pub(crate) enum ModelUsageContext { - Thread(Entity), InlineAssistant, } impl ModelUsageContext { pub fn configured_model(&self, cx: &App) -> Option { match self { - Self::Thread(thread) => thread.read(cx).configured_model(), Self::InlineAssistant => { LanguageModelRegistry::read_global(cx).inline_assistant_model() } diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index b225fbf34058604cfb3f306a9cee14f69bb5edaa..bc309302896eb58c318fa79c108ab9adb5a41ab1 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -6,7 +6,7 @@ pub(crate) mod symbol_context_picker; pub(crate) mod thread_context_picker; use std::ops::Range; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; use anyhow::{Result, anyhow}; @@ -23,9 +23,8 @@ use gpui::{ }; use language::Buffer; use multi_buffer::MultiBufferRow; -use paths::contexts_dir; -use project::{Entry, ProjectPath}; -use prompt_store::{PromptStore, UserPromptId}; +use project::ProjectPath; +use prompt_store::PromptStore; use rules_context_picker::{RulesContextEntry, RulesContextPicker}; use symbol_context_picker::SymbolContextPicker; use thread_context_picker::{ @@ -34,10 +33,8 @@ use thread_context_picker::{ use ui::{ ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*, }; -use uuid::Uuid; use workspace::{Workspace, notifications::NotifyResultExt}; -use crate::AgentPanel; use agent::{ ThreadId, context::RULES_ICON, @@ -664,7 +661,7 @@ pub(crate) fn recent_context_picker_entries( text_thread_store: Option>, workspace: Entity, exclude_paths: &HashSet, - exclude_threads: &HashSet, + _exclude_threads: &HashSet, cx: &App, ) -> Vec { let mut recent = Vec::with_capacity(6); @@ -690,19 +687,13 @@ pub(crate) fn recent_context_picker_entries( }), ); - let active_thread_id = workspace - .panel::(cx) - .and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id())); - if let Some((thread_store, text_thread_store)) = thread_store .and_then(|store| store.upgrade()) .zip(text_thread_store.and_then(|store| store.upgrade())) { let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx) .filter(|(_, thread)| match thread { - ThreadContextEntry::Thread { id, .. } => { - Some(id) != active_thread_id && !exclude_threads.contains(id) - } + ThreadContextEntry::Thread { .. } => false, ThreadContextEntry::Context { .. } => true, }) .collect::>(); @@ -874,15 +865,7 @@ fn fold_toggle( } } -pub enum MentionLink { - File(ProjectPath, Entry), - Symbol(ProjectPath, String), - Selection(ProjectPath, Range), - Fetch(String), - Thread(ThreadId), - TextThread(Arc), - Rule(UserPromptId), -} +pub struct MentionLink; impl MentionLink { const FILE: &str = "@file"; @@ -894,17 +877,6 @@ impl MentionLink { const TEXT_THREAD_URL_PREFIX: &str = "text-thread://"; - const SEPARATOR: &str = ":"; - - pub fn is_valid(url: &str) -> bool { - url.starts_with(Self::FILE) - || url.starts_with(Self::SYMBOL) - || url.starts_with(Self::FETCH) - || url.starts_with(Self::SELECTION) - || url.starts_with(Self::THREAD) - || url.starts_with(Self::RULE) - } - pub fn for_file(file_name: &str, full_path: &str) -> String { format!("[@{}]({}:{})", file_name, Self::FILE, full_path) } @@ -958,75 +930,4 @@ impl MentionLink { pub fn for_rule(rule: &RulesContextEntry) -> String { format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0) } - - pub fn try_parse(link: &str, workspace: &Entity, cx: &App) -> Option { - fn extract_project_path_from_link( - path: &str, - workspace: &Entity, - cx: &App, - ) -> Option { - let path = PathBuf::from(path); - let worktree_name = path.iter().next()?; - let path: PathBuf = path.iter().skip(1).collect(); - let worktree_id = workspace - .read(cx) - .visible_worktrees(cx) - .find(|worktree| worktree.read(cx).root_name() == worktree_name) - .map(|worktree| worktree.read(cx).id())?; - Some(ProjectPath { - worktree_id, - path: path.into(), - }) - } - - let (prefix, argument) = link.split_once(Self::SEPARATOR)?; - match prefix { - Self::FILE => { - let project_path = extract_project_path_from_link(argument, workspace, cx)?; - let entry = workspace - .read(cx) - .project() - .read(cx) - .entry_for_path(&project_path, cx)? - .clone(); - Some(MentionLink::File(project_path, entry)) - } - Self::SYMBOL => { - let (path, symbol) = argument.split_once(Self::SEPARATOR)?; - let project_path = extract_project_path_from_link(path, workspace, cx)?; - Some(MentionLink::Symbol(project_path, symbol.to_string())) - } - Self::SELECTION => { - let (path, line_args) = argument.split_once(Self::SEPARATOR)?; - let project_path = extract_project_path_from_link(path, workspace, cx)?; - - let line_range = { - let (start, end) = line_args - .trim_start_matches('(') - .trim_end_matches(')') - .split_once('-')?; - start.parse::().ok()?..end.parse::().ok()? - }; - - Some(MentionLink::Selection(project_path, line_range)) - } - Self::THREAD => { - if let Some(encoded_filename) = argument.strip_prefix(Self::TEXT_THREAD_URL_PREFIX) - { - let filename = urlencoding::decode(encoded_filename).ok()?; - let path = contexts_dir().join(filename.as_ref()).into(); - Some(MentionLink::TextThread(path)) - } else { - let thread_id = ThreadId::from(argument); - Some(MentionLink::Thread(thread_id)) - } - } - Self::FETCH => Some(MentionLink::Fetch(argument.to_string())), - Self::RULE => { - let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?); - Some(MentionLink::Rule(prompt_id)) - } - _ => None, - } - } } diff --git a/crates/agent_ui/src/context_strip.rs b/crates/agent_ui/src/context_strip.rs index d25d7d35443e6ca7c28bb0894f72c0063f500721..b75b933de40f19557d9dfa83c874c3427773445b 100644 --- a/crates/agent_ui/src/context_strip.rs +++ b/crates/agent_ui/src/context_strip.rs @@ -12,16 +12,19 @@ use agent::{ }; use collections::HashSet; use editor::Editor; -use file_icons::FileIcons; use gpui::{ App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - Subscription, WeakEntity, + Subscription, Task, WeakEntity, }; use itertools::Itertools; use project::ProjectItem; -use std::{path::Path, rc::Rc}; +use rope::Point; +use std::rc::Rc; +use text::ToPoint as _; use ui::{PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; +use util::ResultExt as _; use workspace::Workspace; +use zed_actions::assistant::OpenRulesLibrary; pub struct ContextStrip { context_store: Entity, @@ -121,38 +124,10 @@ impl ContextStrip { fn suggested_context(&self, cx: &App) -> Option { match self.suggest_context_kind { - SuggestContextKind::File => self.suggested_file(cx), SuggestContextKind::Thread => self.suggested_thread(cx), } } - fn suggested_file(&self, cx: &App) -> Option { - let workspace = self.workspace.upgrade()?; - let active_item = workspace.read(cx).active_item(cx)?; - - let editor = active_item.to_any().downcast::().ok()?.read(cx); - let active_buffer_entity = editor.buffer().read(cx).as_singleton()?; - let active_buffer = active_buffer_entity.read(cx); - let project_path = active_buffer.project_path(cx)?; - - if self - .context_store - .read(cx) - .file_path_included(&project_path, cx) - .is_some() - { - return None; - } - - let file_name = active_buffer.file()?.file_name(cx); - let icon_path = FileIcons::get_icon(Path::new(&file_name), cx); - Some(SuggestedContext::File { - name: file_name.to_string_lossy().into_owned().into(), - buffer: active_buffer_entity.downgrade(), - icon_path, - }) - } - fn suggested_thread(&self, cx: &App) -> Option { if !self.context_picker.read(cx).allow_threads() { return None; @@ -161,24 +136,7 @@ impl ContextStrip { let workspace = self.workspace.upgrade()?; let panel = workspace.read(cx).panel::(cx)?.read(cx); - if let Some(active_thread) = panel.active_thread(cx) { - let weak_active_thread = active_thread.downgrade(); - - let active_thread = active_thread.read(cx); - - if self - .context_store - .read(cx) - .includes_thread(active_thread.id()) - { - return None; - } - - Some(SuggestedContext::Thread { - name: active_thread.summary().or_default(), - thread: weak_active_thread, - }) - } else if let Some(active_context_editor) = panel.active_context_editor() { + if let Some(active_context_editor) = panel.active_context_editor() { let context = active_context_editor.read(cx).context(); let weak_context = context.downgrade(); let context = context.read(cx); @@ -328,7 +286,75 @@ impl ContextStrip { return; }; - crate::active_thread::open_context(context, workspace, window, cx); + match context { + AgentContextHandle::File(file_context) => { + if let Some(project_path) = file_context.project_path(cx) { + workspace.update(cx, |workspace, cx| { + workspace + .open_path(project_path, None, true, window, cx) + .detach_and_log_err(cx); + }); + } + } + + AgentContextHandle::Directory(directory_context) => { + let entry_id = directory_context.entry_id; + workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |_project, cx| { + cx.emit(project::Event::RevealInProjectPanel(entry_id)); + }) + }) + } + + AgentContextHandle::Symbol(symbol_context) => { + let buffer = symbol_context.buffer.read(cx); + if let Some(project_path) = buffer.project_path(cx) { + let snapshot = buffer.snapshot(); + let target_position = symbol_context.range.start.to_point(&snapshot); + open_editor_at_position(project_path, target_position, &workspace, window, cx) + .detach(); + } + } + + AgentContextHandle::Selection(selection_context) => { + let buffer = selection_context.buffer.read(cx); + if let Some(project_path) = buffer.project_path(cx) { + let snapshot = buffer.snapshot(); + let target_position = selection_context.range.start.to_point(&snapshot); + + open_editor_at_position(project_path, target_position, &workspace, window, cx) + .detach(); + } + } + + AgentContextHandle::FetchedUrl(fetched_url_context) => { + cx.open_url(&fetched_url_context.url); + } + + AgentContextHandle::Thread(_thread_context) => {} + + AgentContextHandle::TextThread(text_thread_context) => { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = workspace.panel::(cx) { + let context = text_thread_context.context.clone(); + window.defer(cx, move |window, cx| { + panel.update(cx, |panel, cx| { + panel.open_prompt_editor(context, window, cx) + }); + }); + } + }) + } + + AgentContextHandle::Rules(rules_context) => window.dispatch_action( + Box::new(OpenRulesLibrary { + prompt_to_select: Some(rules_context.prompt_id.0), + }), + cx, + ), + + AgentContextHandle::Image(_) => {} + } } fn remove_focused_context( @@ -569,6 +595,31 @@ pub enum ContextStripEvent { impl EventEmitter for ContextStrip {} pub enum SuggestContextKind { - File, Thread, } + +fn open_editor_at_position( + project_path: project::ProjectPath, + target_position: Point, + workspace: &Entity, + window: &mut Window, + cx: &mut App, +) -> Task<()> { + let open_task = workspace.update(cx, |workspace, cx| { + workspace.open_path(project_path, None, true, window, cx) + }); + window.spawn(cx, async move |cx| { + if let Some(active_editor) = open_task + .await + .log_err() + .and_then(|item| item.downcast::()) + { + active_editor + .downgrade() + .update_in(cx, |editor, window, cx| { + editor.go_to_singleton_buffer_point(target_position, window, cx); + }) + .log_err(); + } + }) +} diff --git a/crates/agent_ui/src/debug.rs b/crates/agent_ui/src/debug.rs deleted file mode 100644 index 227528e8ae13dc861fe55da7698c86b485f8ae0a..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/debug.rs +++ /dev/null @@ -1,124 +0,0 @@ -#![allow(unused, dead_code)] - -use client::{ModelRequestUsage, RequestUsage}; -use cloud_llm_client::{Plan, PlanV1, UsageLimit}; -use gpui::Global; -use std::ops::{Deref, DerefMut}; -use ui::prelude::*; - -/// Debug only: Used for testing various account states -/// -/// Use this by initializing it with -/// `cx.set_global(DebugAccountState::default());` somewhere -/// -/// Then call `cx.debug_account()` to get access -#[derive(Clone, Debug)] -pub struct DebugAccountState { - pub enabled: bool, - pub trial_expired: bool, - pub plan: Plan, - pub custom_prompt_usage: ModelRequestUsage, - pub usage_based_billing_enabled: bool, - pub monthly_spending_cap: i32, - pub custom_edit_prediction_usage: UsageLimit, -} - -impl DebugAccountState { - pub fn enabled(&self) -> bool { - self.enabled - } - - pub fn set_enabled(&mut self, enabled: bool) -> &mut Self { - self.enabled = enabled; - self - } - - pub fn set_trial_expired(&mut self, trial_expired: bool) -> &mut Self { - self.trial_expired = trial_expired; - self - } - - pub fn set_plan(&mut self, plan: Plan) -> &mut Self { - self.plan = plan; - self - } - - pub fn set_custom_prompt_usage(&mut self, custom_prompt_usage: ModelRequestUsage) -> &mut Self { - self.custom_prompt_usage = custom_prompt_usage; - self - } - - pub fn set_usage_based_billing_enabled( - &mut self, - usage_based_billing_enabled: bool, - ) -> &mut Self { - self.usage_based_billing_enabled = usage_based_billing_enabled; - self - } - - pub fn set_monthly_spending_cap(&mut self, monthly_spending_cap: i32) -> &mut Self { - self.monthly_spending_cap = monthly_spending_cap; - self - } - - pub fn set_custom_edit_prediction_usage( - &mut self, - custom_edit_prediction_usage: UsageLimit, - ) -> &mut Self { - self.custom_edit_prediction_usage = custom_edit_prediction_usage; - self - } -} - -impl Default for DebugAccountState { - fn default() -> Self { - Self { - enabled: false, - trial_expired: false, - plan: Plan::V1(PlanV1::ZedFree), - custom_prompt_usage: ModelRequestUsage(RequestUsage { - limit: UsageLimit::Unlimited, - amount: 0, - }), - usage_based_billing_enabled: false, - // $50.00 - monthly_spending_cap: 5000, - custom_edit_prediction_usage: UsageLimit::Unlimited, - } - } -} - -impl DebugAccountState { - pub fn get_global(cx: &App) -> &Self { - &cx.global::().0 - } -} - -#[derive(Clone, Debug)] -pub struct GlobalDebugAccountState(pub DebugAccountState); - -impl Global for GlobalDebugAccountState {} - -impl Deref for GlobalDebugAccountState { - type Target = DebugAccountState; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for GlobalDebugAccountState { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -pub trait DebugAccount { - fn debug_account(&self) -> &DebugAccountState; -} - -impl DebugAccount for App { - fn debug_account(&self) -> &DebugAccountState { - &self.global::().0 - } -} diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 0e817ca8073d71022f47b0aa08d34101f622f470..f6347dcb6b80c1b5c939a5c4cd650b9fadf92c62 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -11,8 +11,8 @@ use editor::{ }; use fs::Fs; use gpui::{ - AnyElement, App, Context, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable, - Subscription, TextStyle, WeakEntity, Window, + AnyElement, App, ClipboardEntry, Context, CursorStyle, Entity, EventEmitter, FocusHandle, + Focusable, Subscription, TextStyle, WeakEntity, Window, }; use language_model::{LanguageModel, LanguageModelRegistry}; use parking_lot::Mutex; @@ -272,7 +272,31 @@ impl PromptEditor { } fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context) { - crate::active_thread::attach_pasted_images_as_context(&self.context_store, cx); + let images = cx + .read_from_clipboard() + .map(|item| { + item.into_entries() + .filter_map(|entry| { + if let ClipboardEntry::Image(image) = entry { + Some(image) + } else { + None + } + }) + .collect::>() + }) + .unwrap_or_default(); + + if images.is_empty() { + return; + } + cx.stop_propagation(); + + self.context_store.update(cx, |store, cx| { + for image in images { + store.add_image_instance(Arc::new(image), cx); + } + }); } fn handle_prompt_editor_events( diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index e9a482c5f425f7559df2178f802b390cc53f2f61..a1311f39233c7eaaf0b416401676fb2e43e51a26 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1,157 +1,14 @@ -use std::collections::BTreeMap; -use std::rc::Rc; -use std::sync::Arc; - -use crate::agent_diff::AgentDiffThread; -use crate::agent_model_selector::AgentModelSelector; -use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip}; -use crate::ui::{ - BurnModeTooltip, - preview::{AgentPreview, UsageCallout}, -}; -use agent::history_store::HistoryStore; -use agent::{ - context::{AgentContextKey, ContextLoadResult, load_context}, - context_store::ContextStoreEvent, -}; -use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; -use ai_onboarding::ApiKeysWithProviders; -use buffer_diff::BufferDiff; -use cloud_llm_client::{CompletionIntent, PlanV1}; -use collections::{HashMap, HashSet}; -use editor::actions::{MoveUp, Paste}; +use agent::{context::AgentContextKey, context_store::ContextStoreEvent}; +use agent_settings::AgentProfileId; +use collections::HashMap; use editor::display_map::CreaseId; -use editor::{ - Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, - EditorEvent, EditorMode, EditorStyle, MultiBuffer, -}; -use file_icons::FileIcons; -use fs::Fs; -use futures::future::Shared; -use futures::{FutureExt as _, future}; -use gpui::{ - Animation, AnimationExt, App, Entity, EventEmitter, Focusable, IntoElement, KeyContext, - Subscription, Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, - pulsating_between, -}; -use language::{Buffer, Language, Point}; -use language_model::{ - ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage, MessageContent, - ZED_CLOUD_PROVIDER_ID, -}; -use multi_buffer; -use project::Project; -use prompt_store::PromptStore; -use settings::Settings; -use std::time::Duration; -use theme::ThemeSettings; -use ui::{ - Callout, Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*, -}; -use util::ResultExt as _; -use workspace::{CollaboratorId, Workspace}; -use zed_actions::agent::Chat; -use zed_actions::agent::ToggleModelSelector; - -use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention}; -use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; -use crate::profile_selector::{ProfileProvider, ProfileSelector}; -use crate::{ - ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll, - ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode, - ToggleContextPicker, ToggleProfileSelector, register_agent_preview, -}; -use agent::{ - MessageCrease, Thread, TokenUsageRatio, - context_store::ContextStore, - thread_store::{TextThreadStore, ThreadStore}, -}; - -pub const MIN_EDITOR_LINES: usize = 4; -pub const MAX_EDITOR_LINES: usize = 8; - -#[derive(RegisterComponent)] -pub struct MessageEditor { - thread: Entity, - incompatible_tools_state: Entity, - editor: Entity, - workspace: WeakEntity, - project: Entity, - context_store: Entity, - prompt_store: Option>, - history_store: Option>, - context_strip: Entity, - context_picker_menu_handle: PopoverMenuHandle, - model_selector: Entity, - last_loaded_context: Option, - load_context_task: Option>>, - profile_selector: Entity, - edits_expanded: bool, - editor_is_expanded: bool, - last_estimated_token_count: Option, - update_token_count_task: Option>, - _subscriptions: Vec, -} - -pub(crate) fn create_editor( - workspace: WeakEntity, - context_store: WeakEntity, - thread_store: WeakEntity, - text_thread_store: WeakEntity, - min_lines: usize, - max_lines: Option, - window: &mut Window, - cx: &mut App, -) -> Entity { - let language = Language::new( - language::LanguageConfig { - completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']), - ..Default::default() - }, - None, - ); - - let editor = cx.new(|cx| { - let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - let mut editor = Editor::new( - editor::EditorMode::AutoHeight { - min_lines, - max_lines, - }, - buffer, - None, - window, - cx, - ); - editor.set_placeholder_text("Message the agent – @ to include context", window, cx); - editor.disable_word_completions(); - editor.set_show_indent_guides(false, cx); - editor.set_soft_wrap(); - editor.set_use_modal_editing(true); - editor.set_context_menu_options(ContextMenuOptions { - min_entries_visible: 12, - max_entries_visible: 12, - placement: Some(ContextMenuPlacement::Above), - }); - editor.register_addon(ContextCreasesAddon::new()); - editor.register_addon(MessageEditorAddon::new()); - editor - }); +use editor::{Addon, AnchorRangeExt, Editor}; +use gpui::{App, Entity, Subscription}; +use ui::prelude::*; - let editor_entity = editor.downgrade(); - editor.update(cx, |editor, _| { - editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( - workspace, - context_store, - Some(thread_store), - Some(text_thread_store), - editor_entity, - None, - )))); - }); - editor -} +use crate::context_picker::crease_for_mention; +use crate::profile_selector::ProfileProvider; +use agent::{MessageCrease, Thread, context_store::ContextStore}; impl ProfileProvider for Entity { fn profiles_supported(&self, cx: &App) -> bool { @@ -171,1362 +28,12 @@ impl ProfileProvider for Entity { } } -impl MessageEditor { - pub fn new( - fs: Arc, - workspace: WeakEntity, - context_store: Entity, - prompt_store: Option>, - thread_store: WeakEntity, - text_thread_store: WeakEntity, - history_store: Option>, - thread: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let context_picker_menu_handle = PopoverMenuHandle::default(); - let model_selector_menu_handle = PopoverMenuHandle::default(); - - let editor = create_editor( - workspace.clone(), - context_store.downgrade(), - thread_store.clone(), - text_thread_store.clone(), - MIN_EDITOR_LINES, - Some(MAX_EDITOR_LINES), - window, - cx, - ); - - let context_strip = cx.new(|cx| { - ContextStrip::new( - context_store.clone(), - workspace.clone(), - Some(thread_store.clone()), - Some(text_thread_store.clone()), - context_picker_menu_handle.clone(), - SuggestContextKind::File, - ModelUsageContext::Thread(thread.clone()), - window, - cx, - ) - }); - - let incompatible_tools = cx.new(|cx| IncompatibleToolsState::new(thread.clone(), cx)); - - let subscriptions = vec![ - cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event), - cx.subscribe(&editor, |this, _, event: &EditorEvent, cx| { - if event == &EditorEvent::BufferEdited { - this.handle_message_changed(cx) - } - }), - cx.observe(&context_store, |this, _, cx| { - // When context changes, reload it for token counting. - let _ = this.reload_context(cx); - }), - cx.observe(&thread.read(cx).action_log().clone(), |_, _, cx| { - cx.notify() - }), - ]; - - let model_selector = cx.new(|cx| { - AgentModelSelector::new( - fs.clone(), - model_selector_menu_handle, - editor.focus_handle(cx), - ModelUsageContext::Thread(thread.clone()), - window, - cx, - ) - }); - - let profile_selector = cx.new(|cx| { - ProfileSelector::new(fs, Arc::new(thread.clone()), editor.focus_handle(cx), cx) - }); - - Self { - editor: editor.clone(), - project: thread.read(cx).project().clone(), - thread, - incompatible_tools_state: incompatible_tools, - workspace, - context_store, - prompt_store, - history_store, - context_strip, - context_picker_menu_handle, - load_context_task: None, - last_loaded_context: None, - model_selector, - edits_expanded: false, - editor_is_expanded: false, - profile_selector, - last_estimated_token_count: None, - update_token_count_task: None, - _subscriptions: subscriptions, - } - } - - pub fn context_store(&self) -> &Entity { - &self.context_store - } - - pub fn get_text(&self, cx: &App) -> String { - self.editor.read(cx).text(cx) - } - - pub fn set_text( - &mut self, - text: impl Into>, - window: &mut Window, - cx: &mut Context, - ) { - self.editor.update(cx, |editor, cx| { - editor.set_text(text, window, cx); - }); - } - - pub fn expand_message_editor( - &mut self, - _: &ExpandMessageEditor, - _window: &mut Window, - cx: &mut Context, - ) { - self.set_editor_is_expanded(!self.editor_is_expanded, cx); - } - - fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context) { - self.editor_is_expanded = is_expanded; - self.editor.update(cx, |editor, _| { - if self.editor_is_expanded { - editor.set_mode(EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sized_by_content: false, - }) - } else { - editor.set_mode(EditorMode::AutoHeight { - min_lines: MIN_EDITOR_LINES, - max_lines: Some(MAX_EDITOR_LINES), - }) - } - }); - cx.notify(); - } - - fn toggle_context_picker( - &mut self, - _: &ToggleContextPicker, - window: &mut Window, - cx: &mut Context, - ) { - self.context_picker_menu_handle.toggle(window, cx); - } - - pub fn remove_all_context( - &mut self, - _: &RemoveAllContext, - _window: &mut Window, - cx: &mut Context, - ) { - self.context_store.update(cx, |store, cx| store.clear(cx)); - cx.notify(); - } - - fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context) { - if self.is_editor_empty(cx) { - return; - } - - self.thread.update(cx, |thread, cx| { - thread.cancel_editing(cx); - }); - - if self.thread.read(cx).is_generating() { - self.stop_current_and_send_new_message(window, cx); - return; - } - - self.set_editor_is_expanded(false, cx); - self.send_to_model(window, cx); - - cx.emit(MessageEditorEvent::ScrollThreadToBottom); - cx.notify(); - } - - fn chat_with_follow( - &mut self, - _: &ChatWithFollow, - window: &mut Window, - cx: &mut Context, - ) { - self.workspace - .update(cx, |this, cx| { - this.follow(CollaboratorId::Agent, window, cx) - }) - .log_err(); - - self.chat(&Chat, window, cx); - } - - fn is_editor_empty(&self, cx: &App) -> bool { - self.editor.read(cx).text(cx).trim().is_empty() - } - - pub fn is_editor_fully_empty(&self, cx: &App) -> bool { - self.editor.read(cx).is_empty(cx) - } - - fn send_to_model(&mut self, window: &mut Window, cx: &mut Context) { - let Some(ConfiguredModel { model, .. }) = self - .thread - .update(cx, |thread, cx| thread.get_or_init_configured_model(cx)) - else { - return; - }; - - let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| { - let creases = extract_message_creases(editor, cx); - let text = editor.text(cx); - editor.clear(window, cx); - (text, creases) - }); - - self.last_estimated_token_count.take(); - cx.emit(MessageEditorEvent::EstimatedTokenCount); - - let thread = self.thread.clone(); - let git_store = self.project.read(cx).git_store().clone(); - let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx)); - let context_task = self.reload_context(cx); - let window_handle = window.window_handle(); - - cx.spawn(async move |_this, cx| { - let (checkpoint, loaded_context) = future::join(checkpoint, context_task).await; - let loaded_context = loaded_context.unwrap_or_default(); - - thread - .update(cx, |thread, cx| { - thread.insert_user_message( - user_message, - loaded_context, - checkpoint.ok(), - user_message_creases, - cx, - ); - }) - .log_err(); - - thread - .update(cx, |thread, cx| { - thread.advance_prompt_id(); - thread.send_to_model( - model, - CompletionIntent::UserPrompt, - Some(window_handle), - cx, - ); - }) - .log_err(); - }) - .detach(); - } - - fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context) { - self.thread.update(cx, |thread, cx| { - thread.cancel_editing(cx); - }); - - let canceled = self.thread.update(cx, |thread, cx| { - thread.cancel_last_completion(Some(window.window_handle()), cx) - }); - - if canceled { - self.set_editor_is_expanded(false, cx); - self.send_to_model(window, cx); - } - } - - fn handle_context_strip_event( - &mut self, - _context_strip: &Entity, - event: &ContextStripEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - ContextStripEvent::PickerDismissed - | ContextStripEvent::BlurredEmpty - | ContextStripEvent::BlurredDown => { - let editor_focus_handle = self.editor.focus_handle(cx); - window.focus(&editor_focus_handle); - } - ContextStripEvent::BlurredUp => {} - } - } - - fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) { - if self.context_picker_menu_handle.is_deployed() { - cx.propagate(); - } else if self.context_strip.read(cx).has_context_items(cx) { - self.context_strip.focus_handle(cx).focus(window); - } - } - - fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context) { - crate::active_thread::attach_pasted_images_as_context(&self.context_store, cx); - } - - fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context) { - self.edits_expanded = true; - AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err(); - cx.notify(); - } - - fn handle_edit_bar_expand(&mut self, cx: &mut Context) { - self.edits_expanded = !self.edits_expanded; - cx.notify(); - } - - fn handle_file_click( - &self, - buffer: Entity, - window: &mut Window, - cx: &mut Context, - ) { - if let Ok(diff) = AgentDiffPane::deploy( - AgentDiffThread::Native(self.thread.clone()), - self.workspace.clone(), - window, - cx, - ) { - let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx); - diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx)); - } - } - - pub fn toggle_burn_mode( - &mut self, - _: &ToggleBurnMode, - _window: &mut Window, - cx: &mut Context, - ) { - self.thread.update(cx, |thread, _cx| { - let active_completion_mode = thread.completion_mode(); - - thread.set_completion_mode(match active_completion_mode { - CompletionMode::Burn => CompletionMode::Normal, - CompletionMode::Normal => CompletionMode::Burn, - }); - }); - } - - fn handle_accept_all(&mut self, _window: &mut Window, cx: &mut Context) { - if self.thread.read(cx).has_pending_edit_tool_uses() { - return; - } - - self.thread.update(cx, |thread, cx| { - thread.keep_all_edits(cx); - }); - cx.notify(); - } - - fn handle_reject_all(&mut self, _window: &mut Window, cx: &mut Context) { - if self.thread.read(cx).has_pending_edit_tool_uses() { - return; - } - - // Since there's no reject_all_edits method in the thread API, - // we need to iterate through all buffers and reject their edits - let action_log = self.thread.read(cx).action_log().clone(); - let changed_buffers = action_log.read(cx).changed_buffers(cx); - - for (buffer, _) in changed_buffers { - self.thread.update(cx, |thread, cx| { - let buffer_snapshot = buffer.read(cx); - let start = buffer_snapshot.anchor_before(Point::new(0, 0)); - let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point()); - thread - .reject_edits_in_ranges(buffer, vec![start..end], cx) - .detach(); - }); - } - cx.notify(); - } - - fn handle_reject_file_changes( - &mut self, - buffer: Entity, - _window: &mut Window, - cx: &mut Context, - ) { - if self.thread.read(cx).has_pending_edit_tool_uses() { - return; - } - - self.thread.update(cx, |thread, cx| { - let buffer_snapshot = buffer.read(cx); - let start = buffer_snapshot.anchor_before(Point::new(0, 0)); - let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point()); - thread - .reject_edits_in_ranges(buffer, vec![start..end], cx) - .detach(); - }); - cx.notify(); - } - - fn handle_accept_file_changes( - &mut self, - buffer: Entity, - _window: &mut Window, - cx: &mut Context, - ) { - if self.thread.read(cx).has_pending_edit_tool_uses() { - return; - } - - self.thread.update(cx, |thread, cx| { - let buffer_snapshot = buffer.read(cx); - let start = buffer_snapshot.anchor_before(Point::new(0, 0)); - let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point()); - thread.keep_edits_in_range(buffer, start..end, cx); - }); - cx.notify(); - } - - fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option { - let thread = self.thread.read(cx); - let model = thread.configured_model(); - if !model?.model.supports_burn_mode() { - return None; - } - - let active_completion_mode = thread.completion_mode(); - let burn_mode_enabled = active_completion_mode == CompletionMode::Burn; - let icon = if burn_mode_enabled { - IconName::ZedBurnModeOn - } else { - IconName::ZedBurnMode - }; - - Some( - IconButton::new("burn-mode", icon) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .toggle_state(burn_mode_enabled) - .selected_icon_color(Color::Error) - .on_click(cx.listener(|this, _event, window, cx| { - this.toggle_burn_mode(&ToggleBurnMode, window, cx); - })) - .tooltip(move |_window, cx| { - cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled)) - .into() - }) - .into_any_element(), - ) - } - - fn render_follow_toggle( - &self, - is_model_selected: bool, - cx: &mut Context, - ) -> impl IntoElement { - let following = self - .workspace - .read_with(cx, |workspace, _| { - workspace.is_being_followed(CollaboratorId::Agent) - }) - .unwrap_or(false); - - IconButton::new("follow-agent", IconName::Crosshair) - .disabled(!is_model_selected) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .toggle_state(following) - .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor))) - .tooltip(move |window, cx| { - if following { - Tooltip::for_action("Stop Following Agent", &Follow, window, cx) - } else { - Tooltip::with_meta( - "Follow Agent", - Some(&Follow), - "Track the agent's location as it reads and edits files.", - window, - cx, - ) - } - }) - .on_click(cx.listener(move |this, _, window, cx| { - this.workspace - .update(cx, |workspace, cx| { - if following { - workspace.unfollow(CollaboratorId::Agent, window, cx); - } else { - workspace.follow(CollaboratorId::Agent, window, cx); - } - }) - .ok(); - })) - } - - fn render_editor(&self, window: &mut Window, cx: &mut Context) -> Div { - let thread = self.thread.read(cx); - let model = thread.configured_model(); - - let editor_bg_color = cx.theme().colors().editor_background; - let is_generating = thread.is_generating(); - let focus_handle = self.editor.focus_handle(cx); - - let is_model_selected = model.is_some(); - let is_editor_empty = self.is_editor_empty(cx); - - let incompatible_tools = model - .as_ref() - .map(|model| { - self.incompatible_tools_state.update(cx, |state, cx| { - state.incompatible_tools(&model.model, cx).to_vec() - }) - }) - .unwrap_or_default(); - - let is_editor_expanded = self.editor_is_expanded; - let expand_icon = if is_editor_expanded { - IconName::Minimize - } else { - IconName::Maximize - }; - - v_flex() - .key_context("MessageEditor") - .on_action(cx.listener(Self::chat)) - .on_action(cx.listener(Self::chat_with_follow)) - .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| { - this.profile_selector - .read(cx) - .menu_handle() - .toggle(window, cx); - })) - .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { - this.model_selector - .update(cx, |model_selector, cx| model_selector.toggle(window, cx)); - })) - .on_action(cx.listener(Self::toggle_context_picker)) - .on_action(cx.listener(Self::remove_all_context)) - .on_action(cx.listener(Self::move_up)) - .on_action(cx.listener(Self::expand_message_editor)) - .on_action(cx.listener(Self::toggle_burn_mode)) - .on_action( - cx.listener(|this, _: &KeepAll, window, cx| this.handle_accept_all(window, cx)), - ) - .on_action( - cx.listener(|this, _: &RejectAll, window, cx| this.handle_reject_all(window, cx)), - ) - .capture_action(cx.listener(Self::paste)) - .p_2() - .gap_2() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(editor_bg_color) - .child( - h_flex() - .justify_between() - .child(self.context_strip.clone()) - .when(focus_handle.is_focused(window), |this| { - this.child( - IconButton::new("toggle-height", expand_icon) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - let expand_label = if is_editor_expanded { - "Minimize Message Editor".to_string() - } else { - "Expand Message Editor".to_string() - }; - - Tooltip::for_action_in( - expand_label, - &ExpandMessageEditor, - &focus_handle, - window, - cx, - ) - } - }) - .on_click(cx.listener(|_, _, window, cx| { - window.dispatch_action(Box::new(ExpandMessageEditor), cx); - })), - ) - }), - ) - .child( - v_flex() - .size_full() - .gap_1() - .when(is_editor_expanded, |this| { - this.h(vh(0.8, window)).justify_between() - }) - .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, - font_family: settings.buffer_font.family.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_features: settings.buffer_font.features.clone(), - font_size: font_size.into(), - line_height: line_height.into(), - ..Default::default() - }; - - EditorElement::new( - &self.editor, - EditorStyle { - background: editor_bg_color, - local_player: cx.theme().players().local(), - text: text_style, - syntax: cx.theme().syntax().clone(), - ..Default::default() - }, - ) - .into_any() - }) - .child( - h_flex() - .flex_none() - .flex_wrap() - .justify_between() - .child( - h_flex() - .child(self.render_follow_toggle(is_model_selected, cx)) - .children(self.render_burn_mode_toggle(cx)), - ) - .child( - h_flex() - .gap_1() - .flex_wrap() - .when(!incompatible_tools.is_empty(), |this| { - this.child( - IconButton::new( - "tools-incompatible-warning", - IconName::Warning, - ) - .icon_color(Color::Warning) - .icon_size(IconSize::Small) - .tooltip({ - move |_, cx| { - cx.new(|_| IncompatibleToolsTooltip { - incompatible_tools: incompatible_tools - .clone(), - }) - .into() - } - }), - ) - }) - .child(self.profile_selector.clone()) - .child(self.model_selector.clone()) - .map({ - move |parent| { - if is_generating { - parent - .when(is_editor_empty, |parent| { - parent.child( - IconButton::new( - "stop-generation", - IconName::Stop, - ) - .icon_color(Color::Error) - .style(ButtonStyle::Tinted( - ui::TintColor::Error, - )) - .tooltip(move |window, cx| { - Tooltip::for_action( - "Stop Generation", - &editor::actions::Cancel, - window, - cx, - ) - }) - .on_click({ - let focus_handle = - focus_handle.clone(); - move |_event, window, cx| { - focus_handle.dispatch_action( - &editor::actions::Cancel, - window, - cx, - ); - } - }) - .with_animation( - "pulsating-label", - Animation::new( - Duration::from_secs(2), - ) - .repeat() - .with_easing(pulsating_between( - 0.4, 1.0, - )), - |icon_button, delta| { - icon_button.alpha(delta) - }, - ), - ) - }) - .when(!is_editor_empty, |parent| { - parent.child( - IconButton::new( - "send-message", - IconName::Send, - ) - .icon_color(Color::Accent) - .style(ButtonStyle::Filled) - .disabled(!is_model_selected) - .on_click({ - let focus_handle = - focus_handle.clone(); - move |_event, window, cx| { - focus_handle.dispatch_action( - &Chat, window, cx, - ); - } - }) - .tooltip(move |window, cx| { - Tooltip::for_action( - "Stop and Send New Message", - &Chat, - window, - cx, - ) - }), - ) - }) - } else { - parent.child( - IconButton::new("send-message", IconName::Send) - .icon_color(Color::Accent) - .style(ButtonStyle::Filled) - .disabled( - is_editor_empty || !is_model_selected, - ) - .on_click({ - let focus_handle = focus_handle.clone(); - move |_event, window, cx| { - telemetry::event!( - "Agent Message Sent", - agent = "zed", - ); - focus_handle.dispatch_action( - &Chat, window, cx, - ); - } - }) - .when( - !is_editor_empty && is_model_selected, - |button| { - button.tooltip(move |window, cx| { - Tooltip::for_action( - "Send", &Chat, window, cx, - ) - }) - }, - ) - .when(is_editor_empty, |button| { - button.tooltip(Tooltip::text( - "Type a message to submit", - )) - }) - .when(!is_model_selected, |button| { - button.tooltip(Tooltip::text( - "Select a model to continue", - )) - }), - ) - } - } - }), - ), - ), - ) - } - - fn render_edits_bar( - &self, - changed_buffers: &BTreeMap, Entity>, - window: &mut Window, - cx: &mut Context, - ) -> Div { - let focus_handle = self.editor.focus_handle(cx); - - let editor_bg_color = cx.theme().colors().editor_background; - let border_color = cx.theme().colors().border; - let active_color = cx.theme().colors().element_selected; - let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3)); - - let is_edit_changes_expanded = self.edits_expanded; - let thread = self.thread.read(cx); - let pending_edits = thread.has_pending_edit_tool_uses(); - - const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete."; - - v_flex() - .mt_1() - .mx_2() - .bg(bg_edit_files_disclosure) - .border_1() - .border_b_0() - .border_color(border_color) - .rounded_t_md() - .shadow(vec![gpui::BoxShadow { - color: gpui::black().opacity(0.15), - offset: point(px(1.), px(-1.)), - blur_radius: px(3.), - spread_radius: px(0.), - }]) - .child( - h_flex() - .p_1() - .justify_between() - .when(is_edit_changes_expanded, |this| { - this.border_b_1().border_color(border_color) - }) - .child( - h_flex() - .id("edits-container") - .cursor_pointer() - .w_full() - .gap_1() - .child( - Disclosure::new("edits-disclosure", is_edit_changes_expanded) - .on_click(cx.listener(|this, _, _, cx| { - this.handle_edit_bar_expand(cx) - })), - ) - .map(|this| { - if pending_edits { - this.child( - Label::new(format!( - "Editing {} {}…", - changed_buffers.len(), - if changed_buffers.len() == 1 { - "file" - } else { - "files" - } - )) - .color(Color::Muted) - .size(LabelSize::Small) - .with_animation( - "edit-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.3, 0.7)), - |label, delta| label.alpha(delta), - ), - ) - } else { - this.child( - Label::new("Edits") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - Label::new("•").size(LabelSize::XSmall).color(Color::Muted), - ) - .child( - Label::new(format!( - "{} {}", - changed_buffers.len(), - if changed_buffers.len() == 1 { - "file" - } else { - "files" - } - )) - .size(LabelSize::Small) - .color(Color::Muted), - ) - } - }) - .on_click( - cx.listener(|this, _, _, cx| this.handle_edit_bar_expand(cx)), - ), - ) - .child( - h_flex() - .gap_1() - .child( - IconButton::new("review-changes", IconName::ListTodo) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Review Changes", - &OpenAgentDiff, - &focus_handle, - window, - cx, - ) - } - }) - .on_click(cx.listener(|this, _, window, cx| { - this.handle_review_click(window, cx) - })), - ) - .child(Divider::vertical().color(DividerColor::Border)) - .child( - Button::new("reject-all-changes", "Reject All") - .label_size(LabelSize::Small) - .disabled(pending_edits) - .when(pending_edits, |this| { - this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL)) - }) - .key_binding( - KeyBinding::for_action_in( - &RejectAll, - &focus_handle.clone(), - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .on_click(cx.listener(|this, _, window, cx| { - this.handle_reject_all(window, cx) - })), - ) - .child( - Button::new("accept-all-changes", "Accept All") - .label_size(LabelSize::Small) - .disabled(pending_edits) - .when(pending_edits, |this| { - this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL)) - }) - .key_binding( - KeyBinding::for_action_in( - &KeepAll, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .on_click(cx.listener(|this, _, window, cx| { - this.handle_accept_all(window, cx) - })), - ), - ), - ) - .when(is_edit_changes_expanded, |parent| { - parent.child( - v_flex().children(changed_buffers.iter().enumerate().flat_map( - |(index, (buffer, _diff))| { - let file = buffer.read(cx).file()?; - let path = file.path(); - - let file_path = path.parent().and_then(|parent| { - let parent_str = parent.to_string_lossy(); - - if parent_str.is_empty() { - None - } else { - Some( - Label::new(format!( - "/{}{}", - parent_str, - std::path::MAIN_SEPARATOR_STR - )) - .color(Color::Muted) - .size(LabelSize::XSmall) - .buffer_font(cx), - ) - } - }); - - let file_name = path.file_name().map(|name| { - Label::new(name.to_string_lossy().to_string()) - .size(LabelSize::XSmall) - .buffer_font(cx) - }); - - let file_icon = FileIcons::get_icon(path, cx) - .map(Icon::from_path) - .map(|icon| icon.color(Color::Muted).size(IconSize::Small)) - .unwrap_or_else(|| { - Icon::new(IconName::File) - .color(Color::Muted) - .size(IconSize::Small) - }); - - let overlay_gradient = linear_gradient( - 90., - linear_color_stop(editor_bg_color, 1.), - linear_color_stop(editor_bg_color.opacity(0.2), 0.), - ); - - let element = h_flex() - .group("edited-code") - .id(("file-container", index)) - .relative() - .py_1() - .pl_2() - .pr_1() - .gap_2() - .justify_between() - .bg(editor_bg_color) - .when(index < changed_buffers.len() - 1, |parent| { - parent.border_color(border_color).border_b_1() - }) - .child( - h_flex() - .id(("file-name", index)) - .pr_8() - .gap_1p5() - .max_w_full() - .overflow_x_scroll() - .child(file_icon) - .child( - h_flex() - .gap_0p5() - .children(file_name) - .children(file_path), - ) - .on_click({ - let buffer = buffer.clone(); - cx.listener(move |this, _, window, cx| { - this.handle_file_click(buffer.clone(), window, cx); - }) - }), // TODO: Implement line diff - // .child(Label::new("+").color(Color::Created)) - // .child(Label::new("-").color(Color::Deleted)), - // - ) - .child( - h_flex() - .gap_1() - .visible_on_hover("edited-code") - .child( - Button::new("review", "Review") - .label_size(LabelSize::Small) - .on_click({ - let buffer = buffer.clone(); - cx.listener(move |this, _, window, cx| { - this.handle_file_click( - buffer.clone(), - window, - cx, - ); - }) - }), - ) - .child( - Divider::vertical().color(DividerColor::BorderVariant), - ) - .child( - Button::new("reject-file", "Reject") - .label_size(LabelSize::Small) - .disabled(pending_edits) - .on_click({ - let buffer = buffer.clone(); - cx.listener(move |this, _, window, cx| { - this.handle_reject_file_changes( - buffer.clone(), - window, - cx, - ); - }) - }), - ) - .child( - Button::new("accept-file", "Accept") - .label_size(LabelSize::Small) - .disabled(pending_edits) - .on_click({ - let buffer = buffer.clone(); - cx.listener(move |this, _, window, cx| { - this.handle_accept_file_changes( - buffer.clone(), - window, - cx, - ); - }) - }), - ), - ) - .child( - div() - .id("gradient-overlay") - .absolute() - .h_full() - .w_12() - .top_0() - .bottom_0() - .right(px(152.)) - .bg(overlay_gradient), - ); - - Some(element) - }, - )), - ) - }) - } - - fn is_using_zed_provider(&self, cx: &App) -> bool { - self.thread - .read(cx) - .configured_model() - .is_some_and(|model| model.provider.id() == ZED_CLOUD_PROVIDER_ID) - } - - fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context) -> Option
{ - if !self.is_using_zed_provider(cx) { - return None; - } - - let user_store = self.project.read(cx).user_store().read(cx); - if user_store.is_usage_based_billing_enabled() { - return None; - } - - let plan = user_store - .plan() - .unwrap_or(cloud_llm_client::Plan::V1(PlanV1::ZedFree)); - - let usage = user_store.model_request_usage()?; - - Some( - div() - .child(UsageCallout::new(plan, usage)) - .line_height(line_height), - ) - } - - fn render_token_limit_callout( - &self, - line_height: Pixels, - token_usage_ratio: TokenUsageRatio, - cx: &mut Context, - ) -> Option
{ - let (icon, severity) = if token_usage_ratio == TokenUsageRatio::Exceeded { - (IconName::Close, Severity::Error) - } else { - (IconName::Warning, Severity::Warning) - }; - - let title = if token_usage_ratio == TokenUsageRatio::Exceeded { - "Thread reached the token limit" - } else { - "Thread reaching the token limit soon" - }; - - let description = if self.is_using_zed_provider(cx) { - "To continue, start a new thread from a summary or turn burn mode on." - } else { - "To continue, start a new thread from a summary." - }; - - let callout = Callout::new() - .line_height(line_height) - .severity(severity) - .icon(icon) - .title(title) - .description(description) - .actions_slot( - h_flex() - .gap_0p5() - .when(self.is_using_zed_provider(cx), |this| { - this.child( - IconButton::new("burn-mode-callout", IconName::ZedBurnMode) - .icon_size(IconSize::XSmall) - .on_click(cx.listener(|this, _event, window, cx| { - this.toggle_burn_mode(&ToggleBurnMode, window, cx); - })), - ) - }) - .child( - Button::new("start-new-thread", "Start New Thread") - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - let from_thread_id = Some(this.thread.read(cx).id().clone()); - window.dispatch_action(Box::new(NewThread { from_thread_id }), cx); - })), - ), - ); - - Some( - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child(callout), - ) - } - - pub fn last_estimated_token_count(&self) -> Option { - self.last_estimated_token_count - } - - pub fn is_waiting_to_update_token_count(&self) -> bool { - self.update_token_count_task.is_some() - } - - fn reload_context(&mut self, cx: &mut Context) -> Task> { - let load_task = cx.spawn(async move |this, cx| { - let Ok(load_task) = this.update(cx, |this, cx| { - let new_context = this - .context_store - .read(cx) - .new_context_for_thread(this.thread.read(cx), None); - load_context(new_context, &this.project, &this.prompt_store, cx) - }) else { - return; - }; - let result = load_task.await; - this.update(cx, |this, cx| { - this.last_loaded_context = Some(result); - this.load_context_task = None; - this.message_or_context_changed(false, cx); - }) - .ok(); - }); - // Replace existing load task, if any, causing it to be canceled. - let load_task = load_task.shared(); - self.load_context_task = Some(load_task.clone()); - cx.spawn(async move |this, cx| { - load_task.await; - this.read_with(cx, |this, _cx| this.last_loaded_context.clone()) - .ok() - .flatten() - }) - } - - fn handle_message_changed(&mut self, cx: &mut Context) { - self.message_or_context_changed(true, cx); - } - - fn message_or_context_changed(&mut self, debounce: bool, cx: &mut Context) { - cx.emit(MessageEditorEvent::Changed); - self.update_token_count_task.take(); - - let Some(model) = self.thread.read(cx).configured_model() else { - self.last_estimated_token_count.take(); - return; - }; - - let editor = self.editor.clone(); - - self.update_token_count_task = Some(cx.spawn(async move |this, cx| { - if debounce { - cx.background_executor() - .timer(Duration::from_millis(200)) - .await; - } - - let token_count = if let Some(task) = this - .update(cx, |this, cx| { - let loaded_context = this - .last_loaded_context - .as_ref() - .map(|context_load_result| &context_load_result.loaded_context); - let message_text = editor.read(cx).text(cx); - - if message_text.is_empty() - && loaded_context.is_none_or(|loaded_context| loaded_context.is_empty()) - { - return None; - } - - let mut request_message = LanguageModelRequestMessage { - role: language_model::Role::User, - content: Vec::new(), - cache: false, - }; - - if let Some(loaded_context) = loaded_context { - loaded_context.add_to_request_message(&mut request_message); - } - - if !message_text.is_empty() { - request_message - .content - .push(MessageContent::Text(message_text)); - } - - let request = language_model::LanguageModelRequest { - thread_id: None, - prompt_id: None, - intent: None, - mode: None, - messages: vec![request_message], - tools: vec![], - tool_choice: None, - stop: vec![], - temperature: AgentSettings::temperature_for_model(&model.model, cx), - thinking_allowed: true, - }; - - Some(model.model.count_tokens(request, cx)) - }) - .ok() - .flatten() - { - task.await.log_err() - } else { - Some(0) - }; - - this.update(cx, |this, cx| { - if let Some(token_count) = token_count { - this.last_estimated_token_count = Some(token_count); - cx.emit(MessageEditorEvent::EstimatedTokenCount); - } - this.update_token_count_task.take(); - }) - .ok(); - })); - } -} - #[derive(Default)] pub struct ContextCreasesAddon { creases: HashMap>, _subscription: Option, } -pub struct MessageEditorAddon {} - -impl MessageEditorAddon { - pub fn new() -> Self { - Self {} - } -} - -impl Addon for MessageEditorAddon { - fn to_any(&self) -> &dyn std::any::Any { - self - } - - fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { - Some(self) - } - - fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) { - let settings = agent_settings::AgentSettings::get_global(cx); - if settings.use_modifier_to_send { - key_context.add("use_modifier_to_send"); - } - } -} - impl Addon for ContextCreasesAddon { fn to_any(&self) -> &dyn std::any::Any { self @@ -1631,86 +138,6 @@ pub fn extract_message_creases( }) } -impl EventEmitter for MessageEditor {} - -pub enum MessageEditorEvent { - EstimatedTokenCount, - Changed, - ScrollThreadToBottom, -} - -impl Focusable for MessageEditor { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.editor.focus_handle(cx) - } -} - -impl Render for MessageEditor { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let thread = self.thread.read(cx); - let token_usage_ratio = thread - .total_token_usage() - .map_or(TokenUsageRatio::Normal, |total_token_usage| { - total_token_usage.ratio() - }); - - let burn_mode_enabled = thread.completion_mode() == CompletionMode::Burn; - - let action_log = self.thread.read(cx).action_log(); - let changed_buffers = action_log.read(cx).changed_buffers(cx); - - let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5; - - let has_configured_providers = LanguageModelRegistry::read_global(cx) - .providers() - .iter() - .filter(|provider| { - provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID - }) - .count() - > 0; - - let is_signed_out = self - .workspace - .read_with(cx, |workspace, _| { - workspace.client().status().borrow().is_signed_out() - }) - .unwrap_or(true); - - let has_history = self - .history_store - .as_ref() - .and_then(|hs| hs.update(cx, |hs, cx| !hs.entries(cx).is_empty()).ok()) - .unwrap_or(false) - || self - .thread - .read_with(cx, |thread, _| thread.messages().len() > 0); - - v_flex() - .size_full() - .bg(cx.theme().colors().panel_background) - .when( - !has_history && is_signed_out && has_configured_providers, - |this| this.child(cx.new(ApiKeysWithProviders::new)), - ) - .when(!changed_buffers.is_empty(), |parent| { - parent.child(self.render_edits_bar(&changed_buffers, window, cx)) - }) - .child(self.render_editor(window, cx)) - .children({ - let usage_callout = self.render_usage_callout(line_height, cx); - - if usage_callout.is_some() { - usage_callout - } else if token_usage_ratio != TokenUsageRatio::Normal && !burn_mode_enabled { - self.render_token_limit_callout(line_height, token_usage_ratio, cx) - } else { - None - } - }) - } -} - pub fn insert_message_creases( editor: &mut Editor, message_creases: &[MessageCrease], @@ -1743,70 +170,3 @@ pub fn insert_message_creases( } } } -impl Component for MessageEditor { - fn scope() -> ComponentScope { - ComponentScope::Agent - } - - fn description() -> Option<&'static str> { - Some( - "The composer experience of the Agent Panel. This interface handles context, composing messages, switching profiles, models and more.", - ) - } -} - -impl AgentPreview for MessageEditor { - fn agent_preview( - workspace: WeakEntity, - active_thread: Entity, - window: &mut Window, - cx: &mut App, - ) -> Option { - if let Some(workspace) = workspace.upgrade() { - let fs = workspace.read(cx).app_state().fs.clone(); - let project = workspace.read(cx).project().clone(); - let weak_project = project.downgrade(); - let context_store = cx.new(|_cx| ContextStore::new(weak_project, None)); - let active_thread = active_thread.read(cx); - let thread = active_thread.thread().clone(); - let thread_store = active_thread.thread_store().clone(); - let text_thread_store = active_thread.text_thread_store().clone(); - - let default_message_editor = cx.new(|cx| { - MessageEditor::new( - fs, - workspace.downgrade(), - context_store, - None, - thread_store.downgrade(), - text_thread_store.downgrade(), - None, - thread, - window, - cx, - ) - }); - - Some( - v_flex() - .gap_4() - .children(vec![single_example( - "Default Message Editor", - div() - .w(px(540.)) - .pt_12() - .bg(cx.theme().colors().panel_background) - .border_1() - .border_color(cx.theme().colors().border) - .child(default_message_editor) - .into_any_element(), - )]) - .into_any_element(), - ) - } else { - None - } - } -} - -register_agent_preview!(MessageEditor); diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs index 6ae4a73598a8e0e48509dda7a9bdd5e4fa2ea0ff..07e31ee5dbd3d38319af10c4624b2988192b80e6 100644 --- a/crates/agent_ui/src/profile_selector.rs +++ b/crates/agent_ui/src/profile_selector.rs @@ -1,6 +1,8 @@ use crate::{ManageProfiles, ToggleProfileSelector}; -use agent::agent_profile::{AgentProfile, AvailableProfiles}; -use agent_settings::{AgentDockPosition, AgentProfileId, AgentSettings, builtin_profiles}; +use agent_settings::{ + AgentDockPosition, AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles, + builtin_profiles, +}; use fs::Fs; use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*}; use settings::{Settings as _, SettingsStore, update_settings_file}; diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs deleted file mode 100644 index 73d3b705b74c18e367298cf5ed74852459e68b2e..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/thread_history.rs +++ /dev/null @@ -1,912 +0,0 @@ -use crate::{AgentPanel, RemoveSelectedThread}; -use agent::history_store::{HistoryEntry, HistoryStore}; -use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; -use editor::{Editor, EditorEvent}; -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - App, ClickEvent, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, - UniformListScrollHandle, WeakEntity, Window, uniform_list, -}; -use std::{fmt::Display, ops::Range, sync::Arc}; -use time::{OffsetDateTime, UtcOffset}; -use ui::{ - HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, - Tooltip, prelude::*, -}; -use util::ResultExt; - -pub struct ThreadHistory { - agent_panel: WeakEntity, - history_store: Entity, - scroll_handle: UniformListScrollHandle, - selected_index: usize, - hovered_index: Option, - search_editor: Entity, - all_entries: Arc>, - // When the search is empty, we display date separators between history entries - // This vector contains an enum of either a separator or an actual entry - separated_items: Vec, - // Maps entry indexes to list item indexes - separated_item_indexes: Vec, - _separated_items_task: Option>, - search_state: SearchState, - scrollbar_visibility: bool, - scrollbar_state: ScrollbarState, - _subscriptions: Vec, -} - -enum SearchState { - Empty, - Searching { - query: SharedString, - _task: Task<()>, - }, - Searched { - query: SharedString, - matches: Vec, - }, -} - -enum ListItemType { - BucketSeparator(TimeBucket), - Entry { - index: usize, - format: EntryTimeFormat, - }, -} - -impl ListItemType { - fn entry_index(&self) -> Option { - match self { - ListItemType::BucketSeparator(_) => None, - ListItemType::Entry { index, .. } => Some(*index), - } - } -} - -impl ThreadHistory { - pub(crate) fn new( - agent_panel: WeakEntity, - history_store: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let search_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Search threads...", window, cx); - editor - }); - - let search_editor_subscription = - cx.subscribe(&search_editor, |this, search_editor, event, cx| { - if let EditorEvent::BufferEdited = event { - let query = search_editor.read(cx).text(cx); - this.search(query.into(), cx); - } - }); - - let history_store_subscription = cx.observe(&history_store, |this, _, cx| { - this.update_all_entries(cx); - }); - - let scroll_handle = UniformListScrollHandle::default(); - let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); - - let mut this = Self { - agent_panel, - history_store, - scroll_handle, - selected_index: 0, - hovered_index: None, - search_state: SearchState::Empty, - all_entries: Default::default(), - separated_items: Default::default(), - separated_item_indexes: Default::default(), - search_editor, - scrollbar_visibility: true, - scrollbar_state, - _subscriptions: vec![search_editor_subscription, history_store_subscription], - _separated_items_task: None, - }; - this.update_all_entries(cx); - this - } - - fn update_all_entries(&mut self, cx: &mut Context) { - let new_entries: Arc> = self - .history_store - .update(cx, |store, cx| store.entries(cx)) - .into(); - - self._separated_items_task.take(); - - let mut items = Vec::with_capacity(new_entries.len() + 1); - let mut indexes = Vec::with_capacity(new_entries.len() + 1); - - let bg_task = cx.background_spawn(async move { - let mut bucket = None; - let today = Local::now().naive_local().date(); - - for (index, entry) in new_entries.iter().enumerate() { - let entry_date = entry - .updated_at() - .with_timezone(&Local) - .naive_local() - .date(); - let entry_bucket = TimeBucket::from_dates(today, entry_date); - - if Some(entry_bucket) != bucket { - bucket = Some(entry_bucket); - items.push(ListItemType::BucketSeparator(entry_bucket)); - } - - indexes.push(items.len() as u32); - items.push(ListItemType::Entry { - index, - format: entry_bucket.into(), - }); - } - (new_entries, items, indexes) - }); - - let task = cx.spawn(async move |this, cx| { - let (new_entries, items, indexes) = bg_task.await; - this.update(cx, |this, cx| { - let previously_selected_entry = - this.all_entries.get(this.selected_index).map(|e| e.id()); - - this.all_entries = new_entries; - this.separated_items = items; - this.separated_item_indexes = indexes; - - match &this.search_state { - SearchState::Empty => { - if this.selected_index >= this.all_entries.len() { - this.set_selected_entry_index( - this.all_entries.len().saturating_sub(1), - cx, - ); - } else if let Some(prev_id) = previously_selected_entry - && let Some(new_ix) = this - .all_entries - .iter() - .position(|probe| probe.id() == prev_id) - { - this.set_selected_entry_index(new_ix, cx); - } - } - SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => { - this.search(query.clone(), cx); - } - } - - cx.notify(); - }) - .log_err(); - }); - self._separated_items_task = Some(task); - } - - fn search(&mut self, query: SharedString, cx: &mut Context) { - if query.is_empty() { - self.search_state = SearchState::Empty; - cx.notify(); - return; - } - - let all_entries = self.all_entries.clone(); - - let fuzzy_search_task = cx.background_spawn({ - let query = query.clone(); - let executor = cx.background_executor().clone(); - async move { - let mut candidates = Vec::with_capacity(all_entries.len()); - - for (idx, entry) in all_entries.iter().enumerate() { - match entry { - HistoryEntry::Thread(thread) => { - candidates.push(StringMatchCandidate::new(idx, &thread.summary)); - } - HistoryEntry::Context(context) => { - candidates.push(StringMatchCandidate::new(idx, &context.title)); - } - } - } - - const MAX_MATCHES: usize = 100; - - fuzzy::match_strings( - &candidates, - &query, - false, - true, - MAX_MATCHES, - &Default::default(), - executor, - ) - .await - } - }); - - let task = cx.spawn({ - let query = query.clone(); - async move |this, cx| { - let matches = fuzzy_search_task.await; - - this.update(cx, |this, cx| { - let SearchState::Searching { - query: current_query, - _task, - } = &this.search_state - else { - return; - }; - - if &query == current_query { - this.search_state = SearchState::Searched { - query: query.clone(), - matches, - }; - - this.set_selected_entry_index(0, cx); - cx.notify(); - }; - }) - .log_err(); - } - }); - - self.search_state = SearchState::Searching { query, _task: task }; - cx.notify(); - } - - fn matched_count(&self) -> usize { - match &self.search_state { - SearchState::Empty => self.all_entries.len(), - SearchState::Searching { .. } => 0, - SearchState::Searched { matches, .. } => matches.len(), - } - } - - fn list_item_count(&self) -> usize { - match &self.search_state { - SearchState::Empty => self.separated_items.len(), - SearchState::Searching { .. } => 0, - SearchState::Searched { matches, .. } => matches.len(), - } - } - - fn search_produced_no_matches(&self) -> bool { - match &self.search_state { - SearchState::Empty => false, - SearchState::Searching { .. } => false, - SearchState::Searched { matches, .. } => matches.is_empty(), - } - } - - fn get_match(&self, ix: usize) -> Option<&HistoryEntry> { - match &self.search_state { - SearchState::Empty => self.all_entries.get(ix), - SearchState::Searching { .. } => None, - SearchState::Searched { matches, .. } => matches - .get(ix) - .and_then(|m| self.all_entries.get(m.candidate_id)), - } - } - - pub fn select_previous( - &mut self, - _: &menu::SelectPrevious, - _window: &mut Window, - cx: &mut Context, - ) { - let count = self.matched_count(); - if count > 0 { - if self.selected_index == 0 { - self.set_selected_entry_index(count - 1, cx); - } else { - self.set_selected_entry_index(self.selected_index - 1, cx); - } - } - } - - pub fn select_next( - &mut self, - _: &menu::SelectNext, - _window: &mut Window, - cx: &mut Context, - ) { - let count = self.matched_count(); - if count > 0 { - if self.selected_index == count - 1 { - self.set_selected_entry_index(0, cx); - } else { - self.set_selected_entry_index(self.selected_index + 1, cx); - } - } - } - - fn select_first( - &mut self, - _: &menu::SelectFirst, - _window: &mut Window, - cx: &mut Context, - ) { - let count = self.matched_count(); - if count > 0 { - self.set_selected_entry_index(0, cx); - } - } - - fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - let count = self.matched_count(); - if count > 0 { - self.set_selected_entry_index(count - 1, cx); - } - } - - fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context) { - self.selected_index = entry_index; - - let scroll_ix = match self.search_state { - SearchState::Empty | SearchState::Searching { .. } => self - .separated_item_indexes - .get(entry_index) - .map(|ix| *ix as usize) - .unwrap_or(entry_index + 1), - SearchState::Searched { .. } => entry_index, - }; - - self.scroll_handle - .scroll_to_item(scroll_ix, ScrollStrategy::Top); - - cx.notify(); - } - - fn render_scrollbar(&self, cx: &mut Context) -> Option> { - if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) { - return None; - } - - Some( - div() - .occlude() - .id("thread-history-scroll") - .h_full() - .bg(cx.theme().colors().panel_background.opacity(0.8)) - .border_l_1() - .border_color(cx.theme().colors().border_variant) - .absolute() - .right_1() - .top_0() - .bottom_0() - .w_4() - .pl_1() - .cursor_default() - .on_mouse_move(cx.listener(|_, _, _window, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_scroll_wheel(cx.listener(|_, _, _window, cx| { - cx.notify(); - })) - .children(Scrollbar::vertical(self.scrollbar_state.clone())), - ) - } - - fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - if let Some(entry) = self.get_match(self.selected_index) { - let task_result = match entry { - HistoryEntry::Thread(thread) => self.agent_panel.update(cx, move |this, cx| { - this.open_thread_by_id(&thread.id, window, cx) - }), - HistoryEntry::Context(context) => self.agent_panel.update(cx, move |this, cx| { - this.open_saved_prompt_editor(context.path.clone(), window, cx) - }), - }; - - if let Some(task) = task_result.log_err() { - task.detach_and_log_err(cx); - }; - - cx.notify(); - } - } - - fn remove_selected_thread( - &mut self, - _: &RemoveSelectedThread, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(entry) = self.get_match(self.selected_index) { - let task_result = match entry { - HistoryEntry::Thread(thread) => self - .agent_panel - .update(cx, |this, cx| this.delete_thread(&thread.id, cx)), - HistoryEntry::Context(context) => self - .agent_panel - .update(cx, |this, cx| this.delete_context(context.path.clone(), cx)), - }; - - if let Some(task) = task_result.log_err() { - task.detach_and_log_err(cx); - }; - - cx.notify(); - } - } - - fn list_items( - &mut self, - range: Range, - _window: &mut Window, - cx: &mut Context, - ) -> Vec { - let range_start = range.start; - - match &self.search_state { - SearchState::Empty => self - .separated_items - .get(range) - .iter() - .flat_map(|items| { - items - .iter() - .map(|item| self.render_list_item(item.entry_index(), item, vec![], cx)) - }) - .collect(), - SearchState::Searched { matches, .. } => matches[range] - .iter() - .enumerate() - .map(|(ix, m)| { - self.render_list_item( - Some(range_start + ix), - &ListItemType::Entry { - index: m.candidate_id, - format: EntryTimeFormat::DateAndTime, - }, - m.positions.clone(), - cx, - ) - }) - .collect(), - SearchState::Searching { .. } => { - vec![] - } - } - } - - fn render_list_item( - &self, - list_entry_ix: Option, - item: &ListItemType, - highlight_positions: Vec, - cx: &Context, - ) -> AnyElement { - match item { - ListItemType::Entry { index, format } => match self.all_entries.get(*index) { - Some(entry) => h_flex() - .w_full() - .pb_1() - .child( - HistoryEntryElement::new(entry.clone(), self.agent_panel.clone()) - .highlight_positions(highlight_positions) - .timestamp_format(*format) - .selected(list_entry_ix == Some(self.selected_index)) - .hovered(list_entry_ix == self.hovered_index) - .on_hover(cx.listener(move |this, is_hovered, _window, cx| { - if *is_hovered { - this.hovered_index = list_entry_ix; - } else if this.hovered_index == list_entry_ix { - this.hovered_index = None; - } - - cx.notify(); - })) - .into_any_element(), - ) - .into_any(), - None => Empty.into_any_element(), - }, - ListItemType::BucketSeparator(bucket) => div() - .px(DynamicSpacing::Base06.rems(cx)) - .pt_2() - .pb_1() - .child( - Label::new(bucket.to_string()) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .into_any_element(), - } - } -} - -impl Focusable for ThreadHistory { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.search_editor.focus_handle(cx) - } -} - -impl Render for ThreadHistory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .key_context("ThreadHistory") - .size_full() - .bg(cx.theme().colors().panel_background) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_first)) - .on_action(cx.listener(Self::select_last)) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::remove_selected_thread)) - .when(!self.all_entries.is_empty(), |parent| { - parent.child( - h_flex() - .h(px(41.)) // Match the toolbar perfectly - .w_full() - .py_1() - .px_2() - .gap_2() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border) - .child( - Icon::new(IconName::MagnifyingGlass) - .color(Color::Muted) - .size(IconSize::Small), - ) - .child(self.search_editor.clone()), - ) - }) - .child({ - let view = v_flex() - .id("list-container") - .relative() - .overflow_hidden() - .flex_grow(); - - if self.all_entries.is_empty() { - view.justify_center() - .child( - h_flex().w_full().justify_center().child( - Label::new("You don't have any past threads yet.") - .size(LabelSize::Small), - ), - ) - } else if self.search_produced_no_matches() { - view.justify_center().child( - h_flex().w_full().justify_center().child( - Label::new("No threads match your search.").size(LabelSize::Small), - ), - ) - } else { - view.pr_5() - .child( - uniform_list( - "thread-history", - self.list_item_count(), - cx.processor(|this, range: Range, window, cx| { - this.list_items(range, window, cx) - }), - ) - .p_1() - .track_scroll(self.scroll_handle.clone()) - .flex_grow(), - ) - .when_some(self.render_scrollbar(cx), |div, scrollbar| { - div.child(scrollbar) - }) - } - }) - } -} - -#[derive(IntoElement)] -pub struct HistoryEntryElement { - entry: HistoryEntry, - agent_panel: WeakEntity, - selected: bool, - hovered: bool, - highlight_positions: Vec, - timestamp_format: EntryTimeFormat, - on_hover: Box, -} - -impl HistoryEntryElement { - pub fn new(entry: HistoryEntry, agent_panel: WeakEntity) -> Self { - Self { - entry, - agent_panel, - selected: false, - hovered: false, - highlight_positions: vec![], - timestamp_format: EntryTimeFormat::DateAndTime, - on_hover: Box::new(|_, _, _| {}), - } - } - - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - pub fn hovered(mut self, hovered: bool) -> Self { - self.hovered = hovered; - self - } - - pub fn highlight_positions(mut self, positions: Vec) -> Self { - self.highlight_positions = positions; - self - } - - pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { - self.on_hover = Box::new(on_hover); - self - } - - pub fn timestamp_format(mut self, format: EntryTimeFormat) -> Self { - self.timestamp_format = format; - self - } -} - -impl RenderOnce for HistoryEntryElement { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let (id, summary, timestamp) = match &self.entry { - HistoryEntry::Thread(thread) => ( - thread.id.to_string(), - thread.summary.clone(), - thread.updated_at.timestamp(), - ), - HistoryEntry::Context(context) => ( - context.path.to_string_lossy().to_string(), - context.title.clone(), - context.mtime.timestamp(), - ), - }; - - let thread_timestamp = - self.timestamp_format - .format_timestamp(&self.agent_panel, timestamp, cx); - - ListItem::new(SharedString::from(id)) - .rounded() - .toggle_state(self.selected) - .spacing(ListItemSpacing::Sparse) - .start_slot( - h_flex() - .w_full() - .gap_2() - .justify_between() - .child( - HighlightedLabel::new(summary, self.highlight_positions) - .size(LabelSize::Small) - .truncate(), - ) - .child( - Label::new(thread_timestamp) - .color(Color::Muted) - .size(LabelSize::XSmall), - ), - ) - .on_hover(self.on_hover) - .end_slot::(if self.hovered || self.selected { - Some( - IconButton::new("delete", IconName::Trash) - .shape(IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .tooltip(move |window, cx| { - Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) - }) - .on_click({ - let agent_panel = self.agent_panel.clone(); - - let f: Box = - match &self.entry { - HistoryEntry::Thread(thread) => { - let id = thread.id.clone(); - - Box::new(move |_event, _window, cx| { - agent_panel - .update(cx, |this, cx| { - this.delete_thread(&id, cx) - .detach_and_log_err(cx); - }) - .ok(); - }) - } - HistoryEntry::Context(context) => { - let path = context.path.clone(); - - Box::new(move |_event, _window, cx| { - agent_panel - .update(cx, |this, cx| { - this.delete_context(path.clone(), cx) - .detach_and_log_err(cx); - }) - .ok(); - }) - } - }; - f - }), - ) - } else { - None - }) - .on_click({ - let agent_panel = self.agent_panel.clone(); - - let f: Box = match &self.entry - { - HistoryEntry::Thread(thread) => { - let id = thread.id.clone(); - Box::new(move |_event, window, cx| { - agent_panel - .update(cx, |this, cx| { - this.open_thread_by_id(&id, window, cx) - .detach_and_log_err(cx); - }) - .ok(); - }) - } - HistoryEntry::Context(context) => { - let path = context.path.clone(); - Box::new(move |_event, window, cx| { - agent_panel - .update(cx, |this, cx| { - this.open_saved_prompt_editor(path.clone(), window, cx) - .detach_and_log_err(cx); - }) - .ok(); - }) - } - }; - f - }) - } -} - -#[derive(Clone, Copy)] -pub enum EntryTimeFormat { - DateAndTime, - TimeOnly, -} - -impl EntryTimeFormat { - fn format_timestamp( - &self, - agent_panel: &WeakEntity, - timestamp: i64, - cx: &App, - ) -> String { - let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap(); - let timezone = agent_panel - .read_with(cx, |this, _cx| this.local_timezone()) - .unwrap_or(UtcOffset::UTC); - - match &self { - EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp( - timestamp, - OffsetDateTime::now_utc(), - timezone, - time_format::TimestampFormat::EnhancedAbsolute, - ), - EntryTimeFormat::TimeOnly => time_format::format_time(timestamp), - } - } -} - -impl From for EntryTimeFormat { - fn from(bucket: TimeBucket) -> Self { - match bucket { - TimeBucket::Today => EntryTimeFormat::TimeOnly, - TimeBucket::Yesterday => EntryTimeFormat::TimeOnly, - TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime, - TimeBucket::PastWeek => EntryTimeFormat::DateAndTime, - TimeBucket::All => EntryTimeFormat::DateAndTime, - } - } -} - -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -enum TimeBucket { - Today, - Yesterday, - ThisWeek, - PastWeek, - All, -} - -impl TimeBucket { - fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self { - if date == reference { - return TimeBucket::Today; - } - - if date == reference - TimeDelta::days(1) { - return TimeBucket::Yesterday; - } - - let week = date.iso_week(); - - if reference.iso_week() == week { - return TimeBucket::ThisWeek; - } - - let last_week = (reference - TimeDelta::days(7)).iso_week(); - - if week == last_week { - return TimeBucket::PastWeek; - } - - TimeBucket::All - } -} - -impl Display for TimeBucket { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TimeBucket::Today => write!(f, "Today"), - TimeBucket::Yesterday => write!(f, "Yesterday"), - TimeBucket::ThisWeek => write!(f, "This Week"), - TimeBucket::PastWeek => write!(f, "Past Week"), - TimeBucket::All => write!(f, "All"), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::NaiveDate; - - #[test] - fn test_time_bucket_from_dates() { - let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap(); - - let date = today; - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today); - - let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap(); - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday); - - let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap(); - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek); - - let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap(); - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek); - - let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap(); - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek); - - let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap(); - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek); - - // All: not in this week or last week - let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(); - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All); - - // Test year boundary cases - let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(); - - let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap(); - assert_eq!( - TimeBucket::from_dates(new_year, date), - TimeBucket::Yesterday - ); - - let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap(); - assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek); - } -} diff --git a/crates/agent_ui/src/tool_compatibility.rs b/crates/agent_ui/src/tool_compatibility.rs deleted file mode 100644 index 046c0a4abc5e3ac0130b2af5406cfbb3f977b00c..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/tool_compatibility.rs +++ /dev/null @@ -1,94 +0,0 @@ -use agent::{Thread, ThreadEvent}; -use assistant_tool::{Tool, ToolSource}; -use collections::HashMap; -use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window}; -use language_model::{LanguageModel, LanguageModelToolSchemaFormat}; -use std::sync::Arc; -use ui::prelude::*; - -pub struct IncompatibleToolsState { - cache: HashMap>>, - thread: Entity, - _thread_subscription: Subscription, -} - -impl IncompatibleToolsState { - pub fn new(thread: Entity, cx: &mut Context) -> Self { - let _tool_working_set_subscription = cx.subscribe(&thread, |this, _, event, _| { - if let ThreadEvent::ProfileChanged = event { - this.cache.clear(); - } - }); - - Self { - cache: HashMap::default(), - thread, - _thread_subscription: _tool_working_set_subscription, - } - } - - pub fn incompatible_tools( - &mut self, - model: &Arc, - cx: &App, - ) -> &[Arc] { - self.cache - .entry(model.tool_input_format()) - .or_insert_with(|| { - self.thread - .read(cx) - .profile() - .enabled_tools(cx) - .iter() - .filter(|(_, tool)| tool.input_schema(model.tool_input_format()).is_err()) - .map(|(_, tool)| tool.clone()) - .collect() - }) - } -} - -pub struct IncompatibleToolsTooltip { - pub incompatible_tools: Vec>, -} - -impl Render for IncompatibleToolsTooltip { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - ui::tooltip_container(window, cx, |container, _, cx| { - container - .w_72() - .child(Label::new("Incompatible Tools").size(LabelSize::Small)) - .child( - Label::new( - "This model is incompatible with the following tools from your MCPs:", - ) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - v_flex() - .my_1p5() - .py_0p5() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .children( - self.incompatible_tools - .iter() - .map(|tool| h_flex().gap_4().child(Label::new(tool.name()).size(LabelSize::Small)).map(|parent| - match tool.source() { - ToolSource::Native => parent, - ToolSource::ContextServer { id } => parent.child(Label::new(id).size(LabelSize::Small).color(Color::Muted)), - } - )), - ), - ) - .child(Label::new("What To Do Instead").size(LabelSize::Small)) - .child( - Label::new( - "Every other tool continues to work with this model, but to specifically use those, switch to another model.", - ) - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) - } -} diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 1a3264bd77ccda1a27ffd19f3c61c3635fe78dc9..5363949b904d74d3749c066357e0c60fef19d3b9 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -5,8 +5,8 @@ mod claude_code_onboarding_modal; mod context_pill; mod end_trial_upsell; mod onboarding_modal; -pub mod preview; mod unavailable_editing_tooltip; +mod usage_callout; pub use acp_onboarding_modal::*; pub use agent_notification::*; @@ -16,3 +16,4 @@ pub use context_pill::*; pub use end_trial_upsell::*; pub use onboarding_modal::*; pub use unavailable_editing_tooltip::*; +pub use usage_callout::*; diff --git a/crates/agent_ui/src/ui/context_pill.rs b/crates/agent_ui/src/ui/context_pill.rs index 7c7fbd27f0d4ebe3b5c42cc6c5a244ae6add5614..a015d6305e388b7e16565141f8e42232ba7ce7ac 100644 --- a/crates/agent_ui/src/ui/context_pill.rs +++ b/crates/agent_ui/src/ui/context_pill.rs @@ -13,11 +13,9 @@ use rope::Point; use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container}; use agent::context::{ - AgentContext, AgentContextHandle, ContextId, ContextKind, DirectoryContext, - DirectoryContextHandle, FetchedUrlContext, FileContext, FileContextHandle, ImageContext, - ImageStatus, RulesContext, RulesContextHandle, SelectionContext, SelectionContextHandle, - SymbolContext, SymbolContextHandle, TextThreadContext, TextThreadContextHandle, ThreadContext, - ThreadContextHandle, + AgentContextHandle, ContextId, ContextKind, DirectoryContextHandle, FetchedUrlContext, + FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle, + SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle, }; #[derive(IntoElement)] @@ -317,33 +315,11 @@ impl AddedContext { } } - pub fn new_attached( - context: &AgentContext, - model: Option<&Arc>, - cx: &App, - ) -> AddedContext { - match context { - AgentContext::File(context) => Self::attached_file(context, cx), - AgentContext::Directory(context) => Self::attached_directory(context), - AgentContext::Symbol(context) => Self::attached_symbol(context, cx), - AgentContext::Selection(context) => Self::attached_selection(context, cx), - AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()), - AgentContext::Thread(context) => Self::attached_thread(context), - AgentContext::TextThread(context) => Self::attached_text_thread(context), - AgentContext::Rules(context) => Self::attached_rules(context), - AgentContext::Image(context) => Self::image(context.clone(), model, cx), - } - } - fn pending_file(handle: FileContextHandle, cx: &App) -> Option { let full_path = handle.buffer.read(cx).file()?.full_path(cx); Some(Self::file(handle, &full_path, cx)) } - fn attached_file(context: &FileContext, cx: &App) -> AddedContext { - Self::file(context.handle.clone(), &context.full_path, cx) - } - fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext { let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into(); let (name, parent) = @@ -371,10 +347,6 @@ impl AddedContext { Some(Self::directory(handle, &full_path)) } - fn attached_directory(context: &DirectoryContext) -> AddedContext { - Self::directory(context.handle.clone(), &context.full_path) - } - fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext { let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into(); let (name, parent) = @@ -411,25 +383,6 @@ impl AddedContext { }) } - fn attached_symbol(context: &SymbolContext, cx: &App) -> AddedContext { - let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx); - AddedContext { - kind: ContextKind::Symbol, - name: context.handle.symbol.clone(), - parent: Some(excerpt.file_name_and_range.clone()), - tooltip: None, - icon_path: None, - status: ContextStatus::Ready, - render_hover: { - let text = context.text.clone(); - Some(Rc::new(move |_, cx| { - excerpt.hover_view(text.clone(), cx).into() - })) - }, - handle: AgentContextHandle::Symbol(context.handle.clone()), - } - } - fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option { let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx); Some(AddedContext { @@ -449,25 +402,6 @@ impl AddedContext { }) } - fn attached_selection(context: &SelectionContext, cx: &App) -> AddedContext { - let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx); - AddedContext { - kind: ContextKind::Selection, - name: excerpt.file_name_and_range.clone(), - parent: excerpt.parent_name.clone(), - tooltip: None, - icon_path: excerpt.icon_path.clone(), - status: ContextStatus::Ready, - render_hover: { - let text = context.text.clone(); - Some(Rc::new(move |_, cx| { - excerpt.hover_view(text.clone(), cx).into() - })) - }, - handle: AgentContextHandle::Selection(context.handle.clone()), - } - } - fn fetched_url(context: FetchedUrlContext) -> AddedContext { AddedContext { kind: ContextKind::FetchedUrl, @@ -506,24 +440,6 @@ impl AddedContext { } } - fn attached_thread(context: &ThreadContext) -> AddedContext { - AddedContext { - kind: ContextKind::Thread, - name: context.title.clone(), - parent: None, - tooltip: None, - icon_path: None, - status: ContextStatus::Ready, - render_hover: { - let text = context.text.clone(); - Some(Rc::new(move |_, cx| { - ContextPillHover::new_text(text.clone(), cx).into() - })) - }, - handle: AgentContextHandle::Thread(context.handle.clone()), - } - } - fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext { AddedContext { kind: ContextKind::TextThread, @@ -543,24 +459,6 @@ impl AddedContext { } } - fn attached_text_thread(context: &TextThreadContext) -> AddedContext { - AddedContext { - kind: ContextKind::TextThread, - name: context.title.clone(), - parent: None, - tooltip: None, - icon_path: None, - status: ContextStatus::Ready, - render_hover: { - let text = context.text.clone(); - Some(Rc::new(move |_, cx| { - ContextPillHover::new_text(text.clone(), cx).into() - })) - }, - handle: AgentContextHandle::TextThread(context.handle.clone()), - } - } - fn pending_rules( handle: RulesContextHandle, prompt_store: Option<&Entity>, @@ -584,28 +482,6 @@ impl AddedContext { }) } - fn attached_rules(context: &RulesContext) -> AddedContext { - let title = context - .title - .clone() - .unwrap_or_else(|| "Unnamed Rule".into()); - AddedContext { - kind: ContextKind::Rules, - name: title, - parent: None, - tooltip: None, - icon_path: None, - status: ContextStatus::Ready, - render_hover: { - let text = context.text.clone(); - Some(Rc::new(move |_, cx| { - ContextPillHover::new_text(text.clone(), cx).into() - })) - }, - handle: AgentContextHandle::Rules(context.handle.clone()), - } - } - fn image( context: ImageContext, model: Option<&Arc>, diff --git a/crates/agent_ui/src/ui/preview.rs b/crates/agent_ui/src/ui/preview.rs deleted file mode 100644 index 3ab548dcb4c06ede3b5a716f5247665e01c2b105..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/ui/preview.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod agent_preview; -mod usage_callouts; - -pub use agent_preview::*; -pub use usage_callouts::*; diff --git a/crates/agent_ui/src/ui/preview/agent_preview.rs b/crates/agent_ui/src/ui/preview/agent_preview.rs deleted file mode 100644 index ca189b57a9bfa4eb6ee02a3802e9622058980db7..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/ui/preview/agent_preview.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::sync::OnceLock; - -use collections::HashMap; -use component::ComponentId; -use gpui::{App, Entity, WeakEntity}; -use ui::{AnyElement, Component, ComponentScope, Window}; -use workspace::Workspace; - -use crate::ActiveThread; - -/// Function type for creating agent component previews -pub type PreviewFn = - fn(WeakEntity, Entity, &mut Window, &mut App) -> Option; - -pub struct AgentPreviewFn(fn() -> (ComponentId, PreviewFn)); - -impl AgentPreviewFn { - pub const fn new(f: fn() -> (ComponentId, PreviewFn)) -> Self { - Self(f) - } -} - -inventory::collect!(AgentPreviewFn); - -/// Trait that must be implemented by components that provide agent previews. -pub trait AgentPreview: Component + Sized { - #[allow(unused)] // We can't know this is used due to the distributed slice - fn scope(&self) -> ComponentScope { - ComponentScope::Agent - } - - /// Static method to create a preview for this component type - fn agent_preview( - workspace: WeakEntity, - active_thread: Entity, - window: &mut Window, - cx: &mut App, - ) -> Option; -} - -/// Register an agent preview for the given component type -#[macro_export] -macro_rules! register_agent_preview { - ($type:ty) => { - inventory::submit! { - $crate::ui::preview::AgentPreviewFn::new(|| { - ( - <$type as component::Component>::id(), - <$type as $crate::ui::preview::AgentPreview>::agent_preview, - ) - }) - } - }; -} - -/// Lazy initialized registry of preview functions -static AGENT_PREVIEW_REGISTRY: OnceLock> = OnceLock::new(); - -/// Initialize the agent preview registry if needed -fn get_or_init_registry() -> &'static HashMap { - AGENT_PREVIEW_REGISTRY.get_or_init(|| { - let mut map = HashMap::default(); - for register_fn in inventory::iter::() { - let (id, preview_fn) = (register_fn.0)(); - map.insert(id, preview_fn); - } - map - }) -} - -/// Get a specific agent preview by component ID. -pub fn get_agent_preview( - id: &ComponentId, - workspace: WeakEntity, - active_thread: Entity, - window: &mut Window, - cx: &mut App, -) -> Option { - let registry = get_or_init_registry(); - registry - .get(id) - .and_then(|preview_fn| preview_fn(workspace, active_thread, window, cx)) -} - -/// Get all registered agent previews. -pub fn all_agent_previews() -> Vec { - let registry = get_or_init_registry(); - registry.keys().cloned().collect() -} diff --git a/crates/agent_ui/src/ui/preview/usage_callouts.rs b/crates/agent_ui/src/ui/usage_callout.rs similarity index 100% rename from crates/agent_ui/src/ui/preview/usage_callouts.rs rename to crates/agent_ui/src/ui/usage_callout.rs diff --git a/crates/feature_flags/src/flags.rs b/crates/feature_flags/src/flags.rs index 2278848a194671eab7dccb48aca45a4ad286c7db..eaae9ce42519a8ebfd40d73d242536c5188fa3dd 100644 --- a/crates/feature_flags/src/flags.rs +++ b/crates/feature_flags/src/flags.rs @@ -29,28 +29,3 @@ pub struct JjUiFeatureFlag {} impl FeatureFlag for JjUiFeatureFlag { const NAME: &'static str = "jj-ui"; } - -pub struct GeminiAndNativeFeatureFlag; - -impl FeatureFlag for GeminiAndNativeFeatureFlag { - // This was previously called "acp". - // - // We renamed it because existing builds used it to enable the Claude Code - // integration too, and we'd like to turn Gemini/Native on in new builds - // without enabling Claude Code in old builds. - const NAME: &'static str = "gemini-and-native"; - - fn enabled_for_all() -> bool { - true - } -} - -pub struct ClaudeCodeFeatureFlag; - -impl FeatureFlag for ClaudeCodeFeatureFlag { - const NAME: &'static str = "claude-code"; - - fn enabled_for_all() -> bool { - true - } -} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index cc18e50a9c2f81bc6d84e448ee807a792bcd8fe8..e53dedafd2f27699645662155f154f1220c6cd85 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -27,7 +27,6 @@ agent_settings.workspace = true anyhow.workspace = true askpass.workspace = true assets.workspace = true -assistant_tool.workspace = true assistant_tools.workspace = true audio.workspace = true auto_update.workspace = true @@ -77,7 +76,6 @@ gpui_tokio.workspace = true http_client.workspace = true image_viewer.workspace = true -indoc.workspace = true edit_prediction_button.workspace = true inspector_ui.workspace = true install_cli.workspace = true diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 5b3a951d43aaaf9b79bb33aef6308f45c333dca4..176b176d59b23ed7f605988cf682c9d52dfdb95b 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -3,10 +3,7 @@ //! A view for exploring Zed components. mod persistence; -mod preview_support; -use agent::{TextThreadStore, ThreadStore}; -use agent_ui::ActiveThread; use client::UserStore; use collections::HashMap; use component::{ComponentId, ComponentMetadata, ComponentStatus, components}; @@ -17,14 +14,10 @@ use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle}; use languages::LanguageRegistry; use notifications::status_toast::{StatusToast, ToastIcon}; use persistence::COMPONENT_PREVIEW_DB; -use preview_support::active_thread::{ - load_preview_text_thread_store, load_preview_thread_store, static_active_thread, -}; use project::Project; use std::{iter::Iterator, ops::Range, sync::Arc}; use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*}; use ui_input::SingleLineInput; -use util::ResultExt as _; use workspace::{ AppState, Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items, item::ItemEvent, @@ -74,7 +67,6 @@ pub fn init(app_state: Arc, cx: &mut App) { enum PreviewEntry { AllComponents, - ActiveThread, Separator, Component(ComponentMetadata, Option>), SectionHeader(SharedString), @@ -97,12 +89,10 @@ enum PreviewPage { #[default] AllComponents, Component(ComponentId), - ActiveThread, } struct ComponentPreview { active_page: PreviewPage, - active_thread: Option>, reset_key: usize, component_list: ListState, entries: Vec, @@ -115,8 +105,6 @@ struct ComponentPreview { language_registry: Arc, nav_scroll_handle: UniformListScrollHandle, project: Entity, - text_thread_store: Option>, - thread_store: Option>, user_store: Entity, workspace: WeakEntity, workspace_id: Option, @@ -134,32 +122,6 @@ impl ComponentPreview { window: &mut Window, cx: &mut Context, ) -> anyhow::Result { - let workspace_clone = workspace.clone(); - let project_clone = project.clone(); - - cx.spawn_in(window, async move |entity, cx| { - let thread_store_future = load_preview_thread_store(project_clone.clone(), cx); - let text_thread_store_future = - load_preview_text_thread_store(workspace_clone.clone(), project_clone.clone(), cx); - - let (thread_store_result, text_thread_store_result) = - futures::join!(thread_store_future, text_thread_store_future); - - if let (Some(thread_store), Some(text_thread_store)) = ( - thread_store_result.log_err(), - text_thread_store_result.log_err(), - ) { - entity - .update_in(cx, |this, window, cx| { - this.thread_store = Some(thread_store.clone()); - this.text_thread_store = Some(text_thread_store.clone()); - this.create_active_thread(window, cx); - }) - .ok(); - } - }) - .detach(); - let component_registry = Arc::new(components()); let sorted_components = component_registry.sorted_components(); let selected_index = selected_index.into().unwrap_or(0); @@ -175,7 +137,6 @@ impl ComponentPreview { let mut component_preview = Self { active_page, - active_thread: None, reset_key: 0, component_list, entries: Vec::new(), @@ -188,8 +149,6 @@ impl ComponentPreview { language_registry, nav_scroll_handle: UniformListScrollHandle::new(), project, - text_thread_store: None, - thread_store: None, user_store, workspace, workspace_id: None, @@ -208,43 +167,10 @@ impl ComponentPreview { Ok(component_preview) } - pub fn create_active_thread( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> &mut Self { - let workspace = self.workspace.clone(); - let language_registry = self.language_registry.clone(); - let weak_handle = self.workspace.clone(); - if let Some(workspace) = workspace.upgrade() { - let project = workspace.read(cx).project().clone(); - if let Some((thread_store, text_thread_store)) = self - .thread_store - .clone() - .zip(self.text_thread_store.clone()) - { - let active_thread = static_active_thread( - weak_handle, - project, - language_registry, - thread_store, - text_thread_store, - window, - cx, - ); - self.active_thread = Some(active_thread); - cx.notify(); - } - } - - self - } - pub fn active_page_id(&self, _cx: &App) -> ActivePageId { match &self.active_page { PreviewPage::AllComponents => ActivePageId::default(), PreviewPage::Component(component_id) => ActivePageId(component_id.0.to_string()), - PreviewPage::ActiveThread => ActivePageId("active_thread".to_string()), } } @@ -359,7 +285,6 @@ impl ComponentPreview { // Always show all components first entries.push(PreviewEntry::AllComponents); - entries.push(PreviewEntry::ActiveThread); let mut scopes: Vec<_> = scope_groups .keys() @@ -492,19 +417,6 @@ impl ComponentPreview { })) .into_any_element() } - PreviewEntry::ActiveThread => { - let selected = self.active_page == PreviewPage::ActiveThread; - - ListItem::new(ix) - .child(Label::new("Active Thread")) - .selectable(true) - .toggle_state(selected) - .inset(true) - .on_click(cx.listener(move |this, _, _, cx| { - this.set_active_page(PreviewPage::ActiveThread, cx); - })) - .into_any_element() - } PreviewEntry::Separator => ListItem::new(ix) .disabled(true) .child(div().w_full().py_2().child(Divider::horizontal())) @@ -572,22 +484,7 @@ impl ComponentPreview { ), ); - // Check if the component's scope is Agent - if scope == ComponentScope::Agent { - if let Some(active_thread) = self.active_thread.clone() { - if let Some(element) = agent_ui::get_agent_preview( - &component.id(), - self.workspace.clone(), - active_thread, - window, - cx, - ) { - preview_container = preview_container.child(element); - } else if let Some(preview) = component.preview() { - preview_container = preview_container.children(preview(window, cx)); - } - } - } else if let Some(preview) = component.preview() { + if let Some(preview) = component.preview() { preview_container = preview_container.children(preview(window, cx)); } @@ -629,9 +526,6 @@ impl ComponentPreview { PreviewEntry::AllComponents => { div().w_full().h_0().into_any_element() } - PreviewEntry::ActiveThread => { - div().w_full().h_0().into_any_element() - } PreviewEntry::Separator => div().w_full().h_0().into_any_element(), } }), @@ -655,12 +549,7 @@ impl ComponentPreview { v_flex() .id("render-component-page") .flex_1() - .child(ComponentPreviewPage::new( - component.clone(), - self.workspace.clone(), - self.active_thread.clone(), - self.reset_key, - )) + .child(ComponentPreviewPage::new(component.clone(), self.reset_key)) .into_any_element() } else { v_flex() @@ -672,25 +561,6 @@ impl ComponentPreview { } } - fn render_active_thread(&self, cx: &mut Context) -> impl IntoElement { - v_flex() - .id("render-active-thread") - .size_full() - .child( - div() - .mx_auto() - .w(px(640.)) - .h_full() - .py_8() - .bg(cx.theme().colors().panel_background) - .children(self.active_thread.clone()) - .when_none(&self.active_thread.clone(), |this| { - this.child("No active thread") - }), - ) - .into_any_element() - } - fn test_status_toast(&self, cx: &mut Context) { if let Some(workspace) = self.workspace.upgrade() { workspace.update(cx, |workspace, cx| { @@ -803,9 +673,6 @@ impl Render for ComponentPreview { PreviewPage::Component(id) => self .render_component_page(&id, window, cx) .into_any_element(), - PreviewPage::ActiveThread => { - self.render_active_thread(cx).into_any_element() - } }), ) } @@ -1009,24 +876,18 @@ impl SerializableItem for ComponentPreview { pub struct ComponentPreviewPage { // languages: Arc, component: ComponentMetadata, - workspace: WeakEntity, - active_thread: Option>, reset_key: usize, } impl ComponentPreviewPage { pub fn new( component: ComponentMetadata, - workspace: WeakEntity, - active_thread: Option>, reset_key: usize, // languages: Arc ) -> Self { Self { // languages, component, - workspace, - active_thread, reset_key, } } @@ -1100,23 +961,7 @@ impl ComponentPreviewPage { } fn render_preview(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { - // Try to get agent preview first if we have an active thread - let maybe_agent_preview = if let Some(active_thread) = self.active_thread.as_ref() { - agent_ui::get_agent_preview( - &self.component.id(), - self.workspace.clone(), - active_thread.clone(), - window, - cx, - ) - } else { - None - }; - - let content = if let Some(ag_preview) = maybe_agent_preview { - // Use agent preview if available - ag_preview - } else if let Some(preview) = self.component.preview() { + let content = if let Some(preview) = self.component.preview() { // Fall back to component preview preview(window, cx).unwrap_or_else(|| { div() diff --git a/crates/zed/src/zed/component_preview/preview_support.rs b/crates/zed/src/zed/component_preview/preview_support.rs deleted file mode 100644 index 3b5d6a6df669934e89eab9781fe9d0ee4425b583..0000000000000000000000000000000000000000 --- a/crates/zed/src/zed/component_preview/preview_support.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod active_thread; diff --git a/crates/zed/src/zed/component_preview/preview_support/active_thread.rs b/crates/zed/src/zed/component_preview/preview_support/active_thread.rs deleted file mode 100644 index de98106faeb5b39dfeee5903db55be7fa61f0f3c..0000000000000000000000000000000000000000 --- a/crates/zed/src/zed/component_preview/preview_support/active_thread.rs +++ /dev/null @@ -1,100 +0,0 @@ -use agent::{ContextStore, MessageSegment, TextThreadStore, ThreadStore}; -use agent_ui::ActiveThread; -use anyhow::{Result, anyhow}; -use assistant_tool::ToolWorkingSet; -use gpui::{AppContext, AsyncApp, Entity, Task, WeakEntity}; -use indoc::indoc; -use languages::LanguageRegistry; -use project::Project; -use prompt_store::PromptBuilder; -use std::sync::Arc; -use ui::{App, Window}; -use workspace::Workspace; - -pub fn load_preview_thread_store( - project: Entity, - cx: &mut AsyncApp, -) -> Task>> { - cx.update(|cx| { - ThreadStore::load( - project.clone(), - cx.new(|_| ToolWorkingSet::default()), - None, - Arc::new(PromptBuilder::new(None).unwrap()), - cx, - ) - }) - .unwrap_or(Task::ready(Err(anyhow!("workspace dropped")))) -} - -pub fn load_preview_text_thread_store( - workspace: WeakEntity, - project: Entity, - cx: &mut AsyncApp, -) -> Task>> { - workspace - .update(cx, |_, cx| { - TextThreadStore::new( - project.clone(), - Arc::new(PromptBuilder::new(None).unwrap()), - Default::default(), - cx, - ) - }) - .unwrap_or(Task::ready(Err(anyhow!("workspace dropped")))) -} - -pub fn static_active_thread( - workspace: WeakEntity, - project: Entity, - language_registry: Arc, - thread_store: Entity, - text_thread_store: Entity, - window: &mut Window, - cx: &mut App, -) -> Entity { - let context_store = - cx.new(|_| ContextStore::new(project.downgrade(), Some(thread_store.downgrade()))); - - let thread = thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx)); - thread.update(cx, |thread, cx| { - thread.insert_assistant_message(vec![ - MessageSegment::Text(indoc! {" - I'll help you fix the lifetime error in your `cx.spawn` call. When working with async operations in GPUI, there are specific patterns to follow for proper lifetime management. - - Let's look at what's happening in your code: - - --- - - Let's check the current state of the active_thread.rs file to understand what might have changed: - - --- - - Looking at the implementation of `load_preview_thread_store` and understanding GPUI's async patterns, here's the issue: - - 1. `load_preview_thread_store` returns a `Task>>`, which means it's already a task. - 2. When you call this function inside another `spawn` call, you're nesting tasks incorrectly. - - Here's the correct way to implement this: - - --- - - The problem is in how you're setting up the async closure and trying to reference variables like `window` and `language_registry` that aren't accessible in that scope. - - Here's how to fix it: - "}.to_string()), - ], cx); - }); - cx.new(|cx| { - ActiveThread::new( - thread, - thread_store, - text_thread_store, - context_store, - language_registry, - workspace.clone(), - window, - cx, - ) - }) -}