Detailed changes
@@ -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<Self>,
) {
@@ -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()
@@ -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<Self>) {
@@ -578,11 +584,12 @@ fn recent_context_picker_entries(
context_store: Entity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
workspace: Entity<Workspace>,
+ exclude_path: Option<ProjectPath>,
cx: &App,
) -> Vec<RecentEntry> {
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);
@@ -237,6 +237,7 @@ pub struct ContextPickerCompletionProvider {
context_store: WeakEntity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
editor: WeakEntity<Editor>,
+ excluded_buffer: Option<WeakEntity<Buffer>>,
}
impl ContextPickerCompletionProvider {
@@ -245,12 +246,14 @@ impl ContextPickerCompletionProvider {
context_store: WeakEntity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
editor: WeakEntity<Editor>,
+ exclude_buffer: Option<WeakEntity<Buffer>>,
) -> 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::<Editor>()?
+ .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,
))));
});
@@ -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<BufferCodegen> {
cx: &mut Context<PromptEditor<BufferCodegen>>,
) -> PromptEditor<BufferCodegen> {
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<BufferCodegen> {
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<BufferCodegen> {
context_store.downgrade(),
thread_store.clone(),
prompt_editor_entity,
+ codegen_buffer.as_ref().map(Entity::downgrade),
))));
});
@@ -1035,6 +1045,11 @@ impl PromptEditor<TerminalCodegen> {
);
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<TerminalCodegen> {
context_store.downgrade(),
thread_store.clone(),
prompt_editor_entity,
+ None,
))));
});
@@ -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(),
)
}
@@ -358,6 +358,7 @@ pub struct Thread {
feedback: Option<ThreadFeedback>,
message_feedback: HashMap<MessageId, ThreadFeedback>,
last_auto_capture_at: Option<Instant>,
+ last_received_chunk_at: Option<Instant>,
request_callback: Option<
Box<dyn FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>])>,
>,
@@ -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<bool> {
+ 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<QueueState> {
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);
@@ -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::*;
@@ -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 {
+ Self
+ }
+}
+
+impl Render for MaxModeTooltip {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> 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)
+ )
+ )
+ })
+ }
+}
@@ -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
});
@@ -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),
@@ -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),