diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index c67839225f497840f1c3b64e1088fbb462abbc9d..c9a82201cb211ecf957011457ac758694f2b4e59 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -2,10 +2,9 @@ use crate::context::{AgentContextHandle, RULES_ICON}; use crate::context_picker::{ContextPicker, MentionLink}; use crate::context_store::ContextStore; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; -use crate::message_editor::insert_message_creases; use crate::thread::{ - LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, QueueState, Thread, - ThreadError, ThreadEvent, ThreadFeedback, + LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent, + ThreadFeedback, }; use crate::thread_store::{RulesLoadingError, ThreadStore}; use crate::tool_use::{PendingToolUseStatus, ToolUse}; @@ -1243,7 +1242,6 @@ impl ActiveThread { &mut self, message_id: MessageId, message_segments: &[MessageSegment], - message_creases: &[MessageCrease], window: &mut Window, cx: &mut Context, ) { @@ -1262,7 +1260,6 @@ impl ActiveThread { ); editor.update(cx, |editor, cx| { editor.set_text(message_text.clone(), 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); }); @@ -1710,7 +1707,6 @@ impl ActiveThread { let Some(message) = self.thread.read(cx).message(message_id) else { return Empty.into_any(); }; - let message_creases = message.creases.clone(); let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else { return Empty.into_any(); @@ -1729,33 +1725,13 @@ impl ActiveThread { let tool_uses = thread.tool_uses_for_message(message_id, cx); let has_tool_uses = !tool_uses.is_empty(); let is_generating = thread.is_generating(); + let is_generating_stale = thread.is_generation_stale().unwrap_or(false); let is_first_message = ix == 0; let is_last_message = ix == self.messages.len() - 1; - let show_feedback = thread.is_turn_end(ix); - - let generating_label = is_last_message - .then(|| match (thread.queue_state(), is_generating) { - (Some(QueueState::Sending), _) => Some( - AnimatedLabel::new("Sending") - .size(LabelSize::Small) - .into_any_element(), - ), - (Some(QueueState::Queued { position }), _) => Some( - Label::new(format!("Queue position: {position}")) - .size(LabelSize::Small) - .color(Color::Muted) - .into_any_element(), - ), - (_, true) => Some( - AnimatedLabel::new("Generating") - .size(LabelSize::Small) - .into_any_element(), - ), - _ => None, - }) - .flatten(); + let loading_dots = (is_generating_stale && is_last_message) + .then(|| AnimatedLabel::new("").size(LabelSize::Small)); let editing_message_state = self .editing_message @@ -1778,6 +1754,8 @@ impl ActiveThread { // For all items that should be aligned with the LLM's response. const RESPONSE_PADDING_X: Pixels = px(19.); + let show_feedback = thread.is_turn_end(ix); + let feedback_container = h_flex() .group("feedback_container") .mt_1() @@ -1925,7 +1903,6 @@ impl ActiveThread { open_context(&context, workspace, window, cx); cx.notify(); } - cx.stop_propagation(); } })), ) @@ -2011,13 +1988,15 @@ impl ActiveThread { ) }), ) + .when(editing_message_state.is_none(), |this| { + this.tooltip(Tooltip::text("Click To Edit")) + }) .on_click(cx.listener({ let message_segments = message.segments.clone(); move |this, _, window, cx| { this.start_editing_message( message_id, &message_segments, - &message_creases, window, cx, ); @@ -2053,80 +2032,84 @@ impl ActiveThread { v_flex() .w_full() - .when_some(checkpoint, |parent, checkpoint| { - let mut is_pending = false; - let mut error = None; - if let Some(last_restore_checkpoint) = - self.thread.read(cx).last_restore_checkpoint() - { - if last_restore_checkpoint.message_id() == message_id { - match last_restore_checkpoint { - LastRestoreCheckpoint::Pending { .. } => is_pending = true, - LastRestoreCheckpoint::Error { error: err, .. } => { - error = Some(err.clone()); + .map(|parent| { + if let Some(checkpoint) = checkpoint.filter(|_| is_generating) { + let mut is_pending = false; + let mut error = None; + if let Some(last_restore_checkpoint) = + self.thread.read(cx).last_restore_checkpoint() + { + if last_restore_checkpoint.message_id() == message_id { + match last_restore_checkpoint { + LastRestoreCheckpoint::Pending { .. } => is_pending = true, + LastRestoreCheckpoint::Error { error: err, .. } => { + error = Some(err.clone()); + } } } } - } - let restore_checkpoint_button = - Button::new(("restore-checkpoint", ix), "Restore Checkpoint") - .icon(if error.is_some() { - IconName::XCircle - } else { - IconName::Undo - }) - .icon_size(IconSize::XSmall) - .icon_position(IconPosition::Start) - .icon_color(if error.is_some() { - Some(Color::Error) - } else { - None - }) - .label_size(LabelSize::XSmall) - .disabled(is_pending) - .on_click(cx.listener(move |this, _, _window, cx| { - this.thread.update(cx, |thread, cx| { - thread - .restore_checkpoint(checkpoint.clone(), cx) - .detach_and_log_err(cx); - }); - })); - - let restore_checkpoint_button = if is_pending { - restore_checkpoint_button - .with_animation( - ("pulsating-restore-checkpoint-button", ix), - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.6, 1.)), - |label, delta| label.alpha(delta), - ) - .into_any_element() - } else if let Some(error) = error { - restore_checkpoint_button - .tooltip(Tooltip::text(error.to_string())) - .into_any_element() - } else { - restore_checkpoint_button.into_any_element() - }; + let restore_checkpoint_button = + Button::new(("restore-checkpoint", ix), "Restore Checkpoint") + .icon(if error.is_some() { + IconName::XCircle + } else { + IconName::Undo + }) + .icon_size(IconSize::XSmall) + .icon_position(IconPosition::Start) + .icon_color(if error.is_some() { + Some(Color::Error) + } else { + None + }) + .label_size(LabelSize::XSmall) + .disabled(is_pending) + .on_click(cx.listener(move |this, _, _window, cx| { + this.thread.update(cx, |thread, cx| { + thread + .restore_checkpoint(checkpoint.clone(), cx) + .detach_and_log_err(cx); + }); + })); + + let restore_checkpoint_button = if is_pending { + restore_checkpoint_button + .with_animation( + ("pulsating-restore-checkpoint-button", ix), + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.6, 1.)), + |label, delta| label.alpha(delta), + ) + .into_any_element() + } else if let Some(error) = error { + restore_checkpoint_button + .tooltip(Tooltip::text(error.to_string())) + .into_any_element() + } else { + 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()), - ) + parent.child( + h_flex() + .pt_2p5() + .px_2p5() + .w_full() + .gap_1() + .child(ui::Divider::horizontal()) + .child(restore_checkpoint_button) + .child(ui::Divider::horizontal()), + ) + } else { + parent + } }) .when(is_first_message, |parent| { parent.child(self.render_rules_item(cx)) }) .child(styled_message) - .when_some(generating_label, |this, generating_label| { + .when(is_generating && is_last_message, |this| { this.child( h_flex() .h_8() @@ -2134,7 +2117,7 @@ impl ActiveThread { .mb_4() .ml_4() .py_1p5() - .child(generating_label), + .when_some(loading_dots, |this, loading_dots| this.child(loading_dots)), ) }) .when(show_feedback, move |parent| { @@ -2385,7 +2368,6 @@ impl ActiveThread { let workspace = self.workspace.clone(); move |text, window, cx| { open_markdown_link(text, workspace.clone(), window, cx); - cx.stop_propagation(); } })) .into_any_element() diff --git a/crates/agent/src/context_picker.rs b/crates/agent/src/context_picker.rs index 7ffddd5aa80ee97a67f44116b7fb23671903c3d3..49659d481c8671ab761b36afb613c75fe532c671 100644 --- a/crates/agent/src/context_picker.rs +++ b/crates/agent/src/context_picker.rs @@ -482,7 +482,13 @@ impl ContextPicker { return vec![]; }; - recent_context_picker_entries(context_store, self.thread_store.clone(), workspace, cx) + recent_context_picker_entries( + context_store, + self.thread_store.clone(), + workspace, + None, + cx, + ) } fn notify_current_picker(&mut self, cx: &mut Context) { @@ -578,11 +584,12 @@ fn recent_context_picker_entries( context_store: Entity, thread_store: Option>, workspace: Entity, + exclude_path: Option, cx: &App, ) -> Vec { let mut recent = Vec::with_capacity(6); - - let current_files = context_store.read(cx).file_paths(cx); + let mut current_files = context_store.read(cx).file_paths(cx); + current_files.extend(exclude_path); let workspace = workspace.read(cx); let project = workspace.project().read(cx); diff --git a/crates/agent/src/context_picker/completion_provider.rs b/crates/agent/src/context_picker/completion_provider.rs index 74a533be214224916b43fcef2a0612c5c24778a6..580540435dfb704db68acdd1d9ccdf8771ac830e 100644 --- a/crates/agent/src/context_picker/completion_provider.rs +++ b/crates/agent/src/context_picker/completion_provider.rs @@ -237,6 +237,7 @@ pub struct ContextPickerCompletionProvider { context_store: WeakEntity, thread_store: Option>, editor: WeakEntity, + excluded_buffer: Option>, } impl ContextPickerCompletionProvider { @@ -245,12 +246,14 @@ impl ContextPickerCompletionProvider { context_store: WeakEntity, thread_store: Option>, editor: WeakEntity, + exclude_buffer: Option>, ) -> Self { Self { workspace, context_store, thread_store, editor, + excluded_buffer: exclude_buffer, } } @@ -736,10 +739,18 @@ impl CompletionProvider for ContextPickerCompletionProvider { let MentionCompletion { mode, argument, .. } = state; let query = argument.unwrap_or_else(|| "".to_string()); + let excluded_path = self + .excluded_buffer + .as_ref() + .and_then(WeakEntity::upgrade) + .and_then(|b| b.read(cx).file()) + .map(|file| ProjectPath::from_file(file.as_ref(), cx)); + let recent_entries = recent_context_picker_entries( context_store.clone(), thread_store.clone(), workspace.clone(), + excluded_path.clone(), cx, ); @@ -772,11 +783,17 @@ impl CompletionProvider for ContextPickerCompletionProvider { .into_iter() .filter_map(|mat| match mat { Match::File(FileMatch { mat, is_recent }) => { + let project_path = ProjectPath { + worktree_id: WorktreeId::from_usize(mat.worktree_id), + path: mat.path.clone(), + }; + + if excluded_path.as_ref() == Some(&project_path) { + return None; + } + Some(Self::completion_for_path( - ProjectPath { - worktree_id: WorktreeId::from_usize(mat.worktree_id), - path: mat.path.clone(), - }, + project_path, &mat.path_prefix, is_recent, mat.is_dir, @@ -1138,6 +1155,7 @@ mod tests { "five.txt": "", "six.txt": "", "seven.txt": "", + "eight.txt": "", } }), ) @@ -1164,9 +1182,12 @@ mod tests { separator!("b/five.txt"), separator!("b/six.txt"), separator!("b/seven.txt"), + separator!("b/eight.txt"), ]; + + let mut opened_editors = Vec::new(); for path in paths { - workspace + let buffer = workspace .update_in(&mut cx, |workspace, window, cx| { workspace.open_path( ProjectPath { @@ -1181,6 +1202,7 @@ mod tests { }) .await .unwrap(); + opened_editors.push(buffer); } let editor = workspace.update_in(&mut cx, |workspace, window, cx| { @@ -1210,12 +1232,23 @@ mod tests { let editor_entity = editor.downgrade(); editor.update_in(&mut cx, |editor, window, cx| { + let last_opened_buffer = opened_editors.last().and_then(|editor| { + editor + .downcast::()? + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .as_ref() + .map(Entity::downgrade) + }); window.focus(&editor.focus_handle(cx)); editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new( workspace.downgrade(), context_store.downgrade(), None, editor_entity, + last_opened_buffer, )))); }); diff --git a/crates/agent/src/inline_prompt_editor.rs b/crates/agent/src/inline_prompt_editor.rs index b70996fa8459372a646792c8f1ee603f8a166741..0320b542c9cc47c6e442d1a70aaf032b4cd8e54c 100644 --- a/crates/agent/src/inline_prompt_editor.rs +++ b/crates/agent/src/inline_prompt_editor.rs @@ -12,7 +12,8 @@ use crate::{RemoveAllContext, ToggleContextPicker}; use client::ErrorExt; use collections::VecDeque; use editor::{ - Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, GutterDimensions, MultiBuffer, + ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, + GutterDimensions, MultiBuffer, actions::{MoveDown, MoveUp}, }; use feature_flags::{FeatureFlagAppExt as _, ZedProFeatureFlag}; @@ -849,6 +850,7 @@ impl PromptEditor { cx: &mut Context>, ) -> PromptEditor { let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed); + let codegen_buffer = codegen.read(cx).buffer(cx).read(cx).as_singleton(); let mode = PromptEditorMode::Buffer { id, codegen, @@ -872,8 +874,15 @@ impl PromptEditor { editor.set_show_cursor_when_unfocused(true, cx); editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx); editor.register_addon(ContextCreasesAddon::new()); + editor.set_context_menu_options(ContextMenuOptions { + min_entries_visible: 12, + max_entries_visible: 12, + placement: None, + }); + editor }); + let prompt_editor_entity = prompt_editor.downgrade(); prompt_editor.update(cx, |editor, _| { editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new( @@ -881,6 +890,7 @@ impl PromptEditor { context_store.downgrade(), thread_store.clone(), prompt_editor_entity, + codegen_buffer.as_ref().map(Entity::downgrade), )))); }); @@ -1035,6 +1045,11 @@ impl PromptEditor { ); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx); + editor.set_context_menu_options(ContextMenuOptions { + min_entries_visible: 12, + max_entries_visible: 12, + placement: None, + }); editor }); @@ -1045,6 +1060,7 @@ impl PromptEditor { context_store.downgrade(), thread_store.clone(), prompt_editor_entity, + None, )))); }); diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs index ebb041c8fd2c8ddde16e5a862aaa985e3c42102f..30e3ada988497920a7fed0c9ceb67d86cf54f4c9 100644 --- a/crates/agent/src/message_editor.rs +++ b/crates/agent/src/message_editor.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use crate::assistant_model_selector::{AssistantModelSelector, ModelType}; use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context}; use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip}; -use crate::ui::{AgentPreview, AnimatedLabel}; +use crate::ui::{AgentPreview, AnimatedLabel, MaxModeTooltip}; use buffer_diff::BufferDiff; use collections::{HashMap, HashSet}; use editor::actions::{MoveUp, Paste}; @@ -116,6 +116,7 @@ pub(crate) fn create_editor( context_store, Some(thread_store), editor_entity, + None, )))); }); editor @@ -451,7 +452,7 @@ impl MessageEditor { }); }); })) - .tooltip(Tooltip::text("Toggle Max Mode")) + .tooltip(|_, cx| cx.new(MaxModeTooltip::new).into()) .into_any_element(), ) } diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index f0c5b0d4dd309812af12f43a66f139891474a338..d1611efc7c7abe7e096d42ecb7365497ceb2dd7d 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -358,6 +358,7 @@ pub struct Thread { feedback: Option, message_feedback: HashMap, last_auto_capture_at: Option, + last_received_chunk_at: Option, request_callback: Option< Box])>, >, @@ -419,6 +420,7 @@ impl Thread { feedback: None, message_feedback: HashMap::default(), last_auto_capture_at: None, + last_received_chunk_at: None, request_callback: None, remaining_turns: u32::MAX, configured_model, @@ -525,6 +527,7 @@ impl Thread { feedback: None, message_feedback: HashMap::default(), last_auto_capture_at: None, + last_received_chunk_at: None, request_callback: None, remaining_turns: u32::MAX, configured_model, @@ -632,6 +635,19 @@ impl Thread { !self.pending_completions.is_empty() || !self.all_tools_finished() } + /// Indicates whether streaming of language model events is stale. + /// When `is_generating()` is false, this method returns `None`. + pub fn is_generation_stale(&self) -> Option { + const STALE_THRESHOLD: u128 = 250; + + self.last_received_chunk_at + .map(|instant| instant.elapsed().as_millis() > STALE_THRESHOLD) + } + + fn received_chunk(&mut self) { + self.last_received_chunk_at = Some(Instant::now()); + } + pub fn queue_state(&self) -> Option { self.pending_completions .first() @@ -1328,6 +1344,8 @@ impl Thread { prompt_id: prompt_id.clone(), }; + self.last_received_chunk_at = Some(Instant::now()); + let task = cx.spawn(async move |thread, cx| { let stream_completion_future = model.stream_completion_with_usage(request, &cx); let initial_token_usage = @@ -1398,6 +1416,8 @@ impl Thread { current_token_usage = token_usage; } LanguageModelCompletionEvent::Text(chunk) => { + thread.received_chunk(); + cx.emit(ThreadEvent::ReceivedTextChunk); if let Some(last_message) = thread.messages.last_mut() { if last_message.role == Role::Assistant @@ -1426,6 +1446,8 @@ impl Thread { text: chunk, signature, } => { + thread.received_chunk(); + if let Some(last_message) = thread.messages.last_mut() { if last_message.role == Role::Assistant && !thread.tool_use.has_tool_results(last_message.id) @@ -1512,6 +1534,7 @@ impl Thread { } thread.update(cx, |thread, cx| { + thread.last_received_chunk_at = None; thread .pending_completions .retain(|completion| completion.id != pending_completion_id); diff --git a/crates/agent/src/ui.rs b/crates/agent/src/ui.rs index 4e9465b811822f532a0d1359dbb849a950cc2168..1866928499b2db48889a2c863a0e75d654f13163 100644 --- a/crates/agent/src/ui.rs +++ b/crates/agent/src/ui.rs @@ -2,6 +2,7 @@ mod agent_notification; pub mod agent_preview; mod animated_label; mod context_pill; +mod max_mode_tooltip; mod upsell; mod usage_banner; @@ -9,4 +10,5 @@ pub use agent_notification::*; pub use agent_preview::*; pub use animated_label::*; pub use context_pill::*; +pub use max_mode_tooltip::*; pub use usage_banner::*; diff --git a/crates/agent/src/ui/max_mode_tooltip.rs b/crates/agent/src/ui/max_mode_tooltip.rs new file mode 100644 index 0000000000000000000000000000000000000000..aa4f795ba059d518a03fafa92d0fb590eb989a62 --- /dev/null +++ b/crates/agent/src/ui/max_mode_tooltip.rs @@ -0,0 +1,33 @@ +use gpui::{Context, IntoElement, Render, Window}; +use ui::{prelude::*, tooltip_container}; + +pub struct MaxModeTooltip; + +impl MaxModeTooltip { + pub fn new(_cx: &mut Context) -> Self { + Self + } +} + +impl Render for MaxModeTooltip { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + tooltip_container(_window, cx, |this, _, _| { + this.gap_1() + .child( + h_flex() + .gap_1p5() + .child(Icon::new(IconName::ZedMaxMode).size(IconSize::Small)) + .child(Label::new("Zed's Max Mode")) + ) + .child( + div() + .max_w_72() + .child( + Label::new("This mode enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning, offering an unfettered agentic experience.") + .size(LabelSize::Small) + .color(Color::Muted) + ) + ) + }) + } +} diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index 12733ba038b5546237e471cb59a36a7ee836b472..19ace0877e731f7d25be15d8a474a73c828b5a8a 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -5,7 +5,7 @@ use assistant_settings::AssistantSettings; use client::telemetry::Telemetry; use collections::{HashMap, VecDeque}; use editor::{ - Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer, + ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer, actions::{MoveDown, MoveUp, SelectAll}, }; use fs::Fs; @@ -730,6 +730,11 @@ impl PromptEditor { ); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); editor.set_placeholder_text(Self::placeholder_text(window, cx), cx); + editor.set_context_menu_options(ContextMenuOptions { + min_entries_visible: 12, + max_entries_visible: 12, + placement: None, + }); editor }); diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index ff080e3157ceb84799ff9fd299f48639701655a7..76d42d3812e076f87a8d8e9c2291b6aa5c82e31d 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -304,6 +304,7 @@ impl EditFileToolCard { editor.set_soft_wrap_mode(SoftWrap::None, cx); editor.scroll_manager.set_forbid_vertical_scroll(true); editor.set_show_scrollbars(false, cx); + editor.set_show_indent_guides(false, cx); editor.set_read_only(true); editor.set_show_breakpoints(false, cx); editor.set_show_code_actions(false, cx); @@ -640,7 +641,7 @@ impl ToolCard for EditFileToolCard { .border_t_1() .border_color(border_color) .bg(cx.theme().colors().editor_background) - .child(div().pl_1().child(editor)) + .child(editor) .when( !self.full_height_expanded && is_collapsible, |editor_container| editor_container.child(gradient_overlay), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 54a040e4741c4224a2152ad1019491072d045996..dc4aa48991a88f4a28b9dca0b4af05255f78fa9f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -318,6 +318,13 @@ pub struct ProjectPath { } impl ProjectPath { + pub fn from_file(value: &dyn language::File, cx: &App) -> Self { + ProjectPath { + worktree_id: value.worktree_id(cx), + path: value.path().clone(), + } + } + pub fn from_proto(p: proto::ProjectPath) -> Self { Self { worktree_id: WorktreeId::from_proto(p.worktree_id),