1use crate::{
2 language_model_selector::{LanguageModelSelector, language_model_selector},
3 mention_set::load_external_image_from_path,
4 ui::ModelSelectorTooltip,
5};
6use anyhow::Result;
7use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
8use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
9use client::{proto, zed_urls};
10use collections::{BTreeSet, HashMap, HashSet, hash_map};
11use editor::{
12 Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferOffset,
13 MultiBufferSnapshot, RowExt, ToOffset as _, ToPoint as _,
14 actions::{MoveToEndOfLine, Newline, ShowCompletions},
15 display_map::{
16 BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
17 RenderBlock, ToDisplayPoint,
18 },
19 scroll::ScrollOffset,
20};
21use editor::{FoldPlaceholder, display_map::CreaseId};
22use fs::Fs;
23use futures::FutureExt;
24use gpui::{
25 Action, Animation, AnimationExt, AnyElement, App, ClipboardEntry, ClipboardItem, Empty, Entity,
26 EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement, IntoElement,
27 ParentElement, Pixels, Render, RenderImage, SharedString, Size, StatefulInteractiveElement,
28 Styled, Subscription, Task, WeakEntity, actions, div, img, point, prelude::*,
29 pulsating_between, size,
30};
31use language::{
32 BufferSnapshot, LspAdapterDelegate, ToOffset,
33 language_settings::{SoftWrap, all_language_settings},
34};
35use language_model::{
36 ConfigurationError, IconOrSvg, LanguageModelImage, LanguageModelRegistry, Role,
37};
38use multi_buffer::MultiBufferRow;
39use picker::{Picker, popover_menu::PickerPopoverMenu};
40use project::{Project, Worktree};
41use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};
42use rope::Point;
43use serde::{Deserialize, Serialize};
44use settings::{
45 LanguageModelProviderSetting, LanguageModelSelection, Settings, SettingsStore,
46 update_settings_file,
47};
48use std::{
49 any::{Any, TypeId},
50 cmp,
51 ops::Range,
52 path::{Path, PathBuf},
53 rc::Rc,
54 sync::Arc,
55 time::Duration,
56};
57use text::SelectionGoal;
58use ui::{
59 ButtonLike, CommonAnimationExt, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle,
60 TintColor, Tooltip, prelude::*,
61};
62use util::{ResultExt, maybe};
63use workspace::{
64 CollaboratorId,
65 searchable::{Direction, SearchToken, SearchableItemHandle},
66};
67
68use workspace::{
69 Save, Toast, Workspace,
70 item::{self, FollowableItem, Item},
71 notifications::NotificationId,
72 pane,
73 searchable::{SearchEvent, SearchableItem},
74};
75use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
76
77use crate::CycleFavoriteModels;
78
79use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
80use assistant_text_thread::{
81 CacheStatus, Content, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
82 MessageMetadata, MessageStatus, PendingSlashCommandStatus, TextThread, TextThreadEvent,
83 TextThreadId, ThoughtProcessOutputSection,
84};
85
86actions!(
87 assistant,
88 [
89 /// Sends the current message to the assistant.
90 Assist,
91 /// Confirms and executes the entered slash command.
92 ConfirmCommand,
93 /// Copies code from the assistant's response to the clipboard.
94 CopyCode,
95 /// Cycles between user and assistant message roles.
96 CycleMessageRole,
97 /// Inserts the selected text into the active editor.
98 InsertIntoEditor,
99 /// Splits the conversation at the current cursor position.
100 Split,
101 ]
102);
103
104/// Inserts files that were dragged and dropped into the assistant conversation.
105#[derive(PartialEq, Clone, Action)]
106#[action(namespace = assistant, no_json, no_register)]
107pub enum InsertDraggedFiles {
108 ProjectPaths(Vec<ProjectPath>),
109 ExternalFiles(Vec<PathBuf>),
110}
111
112#[derive(Copy, Clone, Debug, PartialEq)]
113struct ScrollPosition {
114 offset_before_cursor: gpui::Point<ScrollOffset>,
115 cursor: Anchor,
116}
117
118type MessageHeader = MessageMetadata;
119
120#[derive(Clone)]
121enum AssistError {
122 PaymentRequired,
123 Message(SharedString),
124}
125
126pub enum ThoughtProcessStatus {
127 Pending,
128 Completed,
129}
130
131pub trait AgentPanelDelegate {
132 fn active_text_thread_editor(
133 &self,
134 workspace: &mut Workspace,
135 window: &mut Window,
136 cx: &mut Context<Workspace>,
137 ) -> Option<Entity<TextThreadEditor>>;
138
139 fn open_local_text_thread(
140 &self,
141 workspace: &mut Workspace,
142 path: Arc<Path>,
143 window: &mut Window,
144 cx: &mut Context<Workspace>,
145 ) -> Task<Result<()>>;
146
147 fn open_remote_text_thread(
148 &self,
149 workspace: &mut Workspace,
150 text_thread_id: TextThreadId,
151 window: &mut Window,
152 cx: &mut Context<Workspace>,
153 ) -> Task<Result<Entity<TextThreadEditor>>>;
154
155 fn quote_selection(
156 &self,
157 workspace: &mut Workspace,
158 selection_ranges: Vec<Range<Anchor>>,
159 buffer: Entity<MultiBuffer>,
160 window: &mut Window,
161 cx: &mut Context<Workspace>,
162 );
163
164 fn quote_terminal_text(
165 &self,
166 workspace: &mut Workspace,
167 text: String,
168 window: &mut Window,
169 cx: &mut Context<Workspace>,
170 );
171}
172
173impl dyn AgentPanelDelegate {
174 /// Returns the global [`AssistantPanelDelegate`], if it exists.
175 pub fn try_global(cx: &App) -> Option<Arc<Self>> {
176 cx.try_global::<GlobalAssistantPanelDelegate>()
177 .map(|global| global.0.clone())
178 }
179
180 /// Sets the global [`AssistantPanelDelegate`].
181 pub fn set_global(delegate: Arc<Self>, cx: &mut App) {
182 cx.set_global(GlobalAssistantPanelDelegate(delegate));
183 }
184}
185
186struct GlobalAssistantPanelDelegate(Arc<dyn AgentPanelDelegate>);
187
188impl Global for GlobalAssistantPanelDelegate {}
189
190pub struct TextThreadEditor {
191 text_thread: Entity<TextThread>,
192 fs: Arc<dyn Fs>,
193 slash_commands: Arc<SlashCommandWorkingSet>,
194 workspace: WeakEntity<Workspace>,
195 project: Entity<Project>,
196 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
197 editor: Entity<Editor>,
198 pending_thought_process: Option<(CreaseId, language::Anchor)>,
199 blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
200 image_blocks: HashSet<CustomBlockId>,
201 scroll_position: Option<ScrollPosition>,
202 remote_id: Option<workspace::ViewId>,
203 pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
204 invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
205 _subscriptions: Vec<Subscription>,
206 last_error: Option<AssistError>,
207 pub(crate) slash_menu_handle:
208 PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
209 // dragged_file_worktrees is used to keep references to worktrees that were added
210 // when the user drag/dropped an external file onto the context editor. Since
211 // the worktree is not part of the project panel, it would be dropped as soon as
212 // the file is opened. In order to keep the worktree alive for the duration of the
213 // context editor, we keep a reference here.
214 dragged_file_worktrees: Vec<Entity<Worktree>>,
215 language_model_selector: Entity<LanguageModelSelector>,
216 language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
217}
218
219const MAX_TAB_TITLE_LEN: usize = 16;
220
221impl TextThreadEditor {
222 pub fn init(cx: &mut App) {
223 workspace::FollowableViewRegistry::register::<TextThreadEditor>(cx);
224
225 cx.observe_new(
226 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
227 workspace
228 .register_action(TextThreadEditor::quote_selection)
229 .register_action(TextThreadEditor::insert_selection)
230 .register_action(TextThreadEditor::copy_code)
231 .register_action(TextThreadEditor::handle_insert_dragged_files);
232 },
233 )
234 .detach();
235 }
236
237 pub fn for_text_thread(
238 text_thread: Entity<TextThread>,
239 fs: Arc<dyn Fs>,
240 workspace: WeakEntity<Workspace>,
241 project: Entity<Project>,
242 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
243 window: &mut Window,
244 cx: &mut Context<Self>,
245 ) -> Self {
246 let completion_provider = SlashCommandCompletionProvider::new(
247 text_thread.read(cx).slash_commands().clone(),
248 Some(cx.entity().downgrade()),
249 Some(workspace.clone()),
250 );
251
252 let editor = cx.new(|cx| {
253 let mut editor =
254 Editor::for_buffer(text_thread.read(cx).buffer().clone(), None, window, cx);
255 editor.disable_scrollbars_and_minimap(window, cx);
256 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
257 editor.set_show_line_numbers(false, cx);
258 editor.set_show_git_diff_gutter(false, cx);
259 editor.set_show_code_actions(false, cx);
260 editor.set_show_runnables(false, cx);
261 editor.set_show_breakpoints(false, cx);
262 editor.set_show_wrap_guides(false, cx);
263 editor.set_show_indent_guides(false, cx);
264 editor.set_completion_provider(Some(Rc::new(completion_provider)));
265 editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::Never);
266 editor.set_collaboration_hub(Box::new(project.clone()));
267
268 let show_edit_predictions = all_language_settings(None, cx)
269 .edit_predictions
270 .enabled_in_text_threads;
271
272 editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
273
274 editor
275 });
276
277 let _subscriptions = vec![
278 cx.observe(&text_thread, |_, _, cx| cx.notify()),
279 cx.subscribe_in(&text_thread, window, Self::handle_text_thread_event),
280 cx.subscribe_in(&editor, window, Self::handle_editor_event),
281 cx.subscribe_in(&editor, window, Self::handle_editor_search_event),
282 cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
283 ];
284
285 let slash_command_sections = text_thread
286 .read(cx)
287 .slash_command_output_sections()
288 .to_vec();
289 let thought_process_sections = text_thread
290 .read(cx)
291 .thought_process_output_sections()
292 .to_vec();
293 let slash_commands = text_thread.read(cx).slash_commands().clone();
294 let focus_handle = editor.read(cx).focus_handle(cx);
295
296 let mut this = Self {
297 text_thread,
298 slash_commands,
299 editor,
300 lsp_adapter_delegate,
301 blocks: Default::default(),
302 image_blocks: Default::default(),
303 scroll_position: None,
304 remote_id: None,
305 pending_thought_process: None,
306 fs: fs.clone(),
307 workspace,
308 project,
309 pending_slash_command_creases: HashMap::default(),
310 invoked_slash_command_creases: HashMap::default(),
311 _subscriptions,
312 last_error: None,
313 slash_menu_handle: Default::default(),
314 dragged_file_worktrees: Vec::new(),
315 language_model_selector: cx.new(|cx| {
316 language_model_selector(
317 |cx| LanguageModelRegistry::read_global(cx).default_model(),
318 {
319 let fs = fs.clone();
320 move |model, cx| {
321 update_settings_file(fs.clone(), cx, move |settings, _| {
322 let provider = model.provider_id().0.to_string();
323 let model_id = model.id().0.to_string();
324 settings.agent.get_or_insert_default().set_model(
325 LanguageModelSelection {
326 provider: LanguageModelProviderSetting(provider),
327 model: model_id,
328 enable_thinking: model.supports_thinking(),
329 effort: model
330 .default_effort_level()
331 .map(|effort| effort.value.to_string()),
332 },
333 )
334 });
335 }
336 },
337 {
338 let fs = fs.clone();
339 move |model, should_be_favorite, cx| {
340 crate::favorite_models::toggle_in_settings(
341 model,
342 should_be_favorite,
343 fs.clone(),
344 cx,
345 );
346 }
347 },
348 true, // Use popover styles for picker
349 focus_handle,
350 window,
351 cx,
352 )
353 }),
354 language_model_selector_menu_handle: PopoverMenuHandle::default(),
355 };
356 this.update_message_headers(cx);
357 this.update_image_blocks(cx);
358 this.insert_slash_command_output_sections(slash_command_sections, false, window, cx);
359 this.insert_thought_process_output_sections(
360 thought_process_sections
361 .into_iter()
362 .map(|section| (section, ThoughtProcessStatus::Completed)),
363 window,
364 cx,
365 );
366 this
367 }
368
369 fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
370 self.editor.update(cx, |editor, cx| {
371 let show_edit_predictions = all_language_settings(None, cx)
372 .edit_predictions
373 .enabled_in_text_threads;
374
375 editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
376 });
377 }
378
379 pub fn text_thread(&self) -> &Entity<TextThread> {
380 &self.text_thread
381 }
382
383 pub fn editor(&self) -> &Entity<Editor> {
384 &self.editor
385 }
386
387 pub fn insert_default_prompt(&mut self, window: &mut Window, cx: &mut Context<Self>) {
388 let command_name = DefaultSlashCommand.name();
389 self.editor.update(cx, |editor, cx| {
390 editor.insert(&format!("/{command_name}\n\n"), window, cx)
391 });
392 let command = self.text_thread.update(cx, |text_thread, cx| {
393 text_thread.reparse(cx);
394 text_thread.parsed_slash_commands()[0].clone()
395 });
396 self.run_command(
397 command.source_range,
398 &command.name,
399 &command.arguments,
400 false,
401 self.workspace.clone(),
402 window,
403 cx,
404 );
405 }
406
407 fn assist(&mut self, _: &Assist, window: &mut Window, cx: &mut Context<Self>) {
408 if self.sending_disabled(cx) {
409 return;
410 }
411 telemetry::event!("Agent Message Sent", agent = "zed-text");
412 self.send_to_model(window, cx);
413 }
414
415 fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
416 self.last_error = None;
417 if let Some(user_message) = self
418 .text_thread
419 .update(cx, |text_thread, cx| text_thread.assist(cx))
420 {
421 let new_selection = {
422 let cursor = user_message
423 .start
424 .to_offset(self.text_thread.read(cx).buffer().read(cx));
425 MultiBufferOffset(cursor)..MultiBufferOffset(cursor)
426 };
427 self.editor.update(cx, |editor, cx| {
428 editor.change_selections(Default::default(), window, cx, |selections| {
429 selections.select_ranges([new_selection])
430 });
431 });
432 // Avoid scrolling to the new cursor position so the assistant's output is stable.
433 cx.defer_in(window, |this, _, _| this.scroll_position = None);
434 }
435
436 cx.notify();
437 }
438
439 fn cancel(
440 &mut self,
441 _: &editor::actions::Cancel,
442 _window: &mut Window,
443 cx: &mut Context<Self>,
444 ) {
445 self.last_error = None;
446
447 if self
448 .text_thread
449 .update(cx, |text_thread, cx| text_thread.cancel_last_assist(cx))
450 {
451 return;
452 }
453
454 cx.propagate();
455 }
456
457 fn cycle_message_role(
458 &mut self,
459 _: &CycleMessageRole,
460 _window: &mut Window,
461 cx: &mut Context<Self>,
462 ) {
463 let cursors = self.cursors(cx);
464 self.text_thread.update(cx, |text_thread, cx| {
465 let messages = text_thread
466 .messages_for_offsets(cursors.into_iter().map(|cursor| cursor.0), cx)
467 .into_iter()
468 .map(|message| message.id)
469 .collect();
470 text_thread.cycle_message_roles(messages, cx)
471 });
472 }
473
474 fn cursors(&self, cx: &mut App) -> Vec<MultiBufferOffset> {
475 let selections = self.editor.update(cx, |editor, cx| {
476 editor
477 .selections
478 .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
479 });
480 selections
481 .into_iter()
482 .map(|selection| selection.head())
483 .collect()
484 }
485
486 pub fn insert_command(&mut self, name: &str, window: &mut Window, cx: &mut Context<Self>) {
487 if let Some(command) = self.slash_commands.command(name, cx) {
488 self.editor.update(cx, |editor, cx| {
489 editor.transact(window, cx, |editor, window, cx| {
490 editor.change_selections(Default::default(), window, cx, |s| s.try_cancel());
491 let snapshot = editor.buffer().read(cx).snapshot(cx);
492 let newest_cursor = editor
493 .selections
494 .newest::<Point>(&editor.display_snapshot(cx))
495 .head();
496 if newest_cursor.column > 0
497 || snapshot
498 .chars_at(newest_cursor)
499 .next()
500 .is_some_and(|ch| ch != '\n')
501 {
502 editor.move_to_end_of_line(
503 &MoveToEndOfLine {
504 stop_at_soft_wraps: false,
505 },
506 window,
507 cx,
508 );
509 editor.newline(&Newline, window, cx);
510 }
511
512 editor.insert(&format!("/{name}"), window, cx);
513 if command.accepts_arguments() {
514 editor.insert(" ", window, cx);
515 editor.show_completions(&ShowCompletions, window, cx);
516 }
517 });
518 });
519 if !command.requires_argument() {
520 self.confirm_command(&ConfirmCommand, window, cx);
521 }
522 }
523 }
524
525 pub fn confirm_command(
526 &mut self,
527 _: &ConfirmCommand,
528 window: &mut Window,
529 cx: &mut Context<Self>,
530 ) {
531 if self.editor.read(cx).has_visible_completions_menu() {
532 return;
533 }
534
535 let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
536 let mut commands_by_range = HashMap::default();
537 let workspace = self.workspace.clone();
538 self.text_thread.update(cx, |text_thread, cx| {
539 text_thread.reparse(cx);
540 for selection in selections.iter() {
541 if let Some(command) =
542 text_thread.pending_command_for_position(selection.head().text_anchor, cx)
543 {
544 commands_by_range
545 .entry(command.source_range.clone())
546 .or_insert_with(|| command.clone());
547 }
548 }
549 });
550
551 if commands_by_range.is_empty() {
552 cx.propagate();
553 } else {
554 for command in commands_by_range.into_values() {
555 self.run_command(
556 command.source_range,
557 &command.name,
558 &command.arguments,
559 true,
560 workspace.clone(),
561 window,
562 cx,
563 );
564 }
565 cx.stop_propagation();
566 }
567 }
568
569 pub fn run_command(
570 &mut self,
571 command_range: Range<language::Anchor>,
572 name: &str,
573 arguments: &[String],
574 ensure_trailing_newline: bool,
575 workspace: WeakEntity<Workspace>,
576 window: &mut Window,
577 cx: &mut Context<Self>,
578 ) {
579 if let Some(command) = self.slash_commands.command(name, cx) {
580 let text_thread = self.text_thread.read(cx);
581 let sections = text_thread
582 .slash_command_output_sections()
583 .iter()
584 .filter(|section| section.is_valid(text_thread.buffer().read(cx)))
585 .cloned()
586 .collect::<Vec<_>>();
587 let snapshot = text_thread.buffer().read(cx).snapshot();
588 let output = command.run(
589 arguments,
590 §ions,
591 snapshot,
592 workspace,
593 self.lsp_adapter_delegate.clone(),
594 window,
595 cx,
596 );
597 self.text_thread.update(cx, |text_thread, cx| {
598 text_thread.insert_command_output(
599 command_range,
600 name,
601 output,
602 ensure_trailing_newline,
603 cx,
604 )
605 });
606 }
607 }
608
609 fn handle_text_thread_event(
610 &mut self,
611 _: &Entity<TextThread>,
612 event: &TextThreadEvent,
613 window: &mut Window,
614 cx: &mut Context<Self>,
615 ) {
616 let text_thread_editor = cx.entity().downgrade();
617
618 match event {
619 TextThreadEvent::MessagesEdited => {
620 self.update_message_headers(cx);
621 self.update_image_blocks(cx);
622 self.text_thread.update(cx, |text_thread, cx| {
623 text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
624 });
625 }
626 TextThreadEvent::SummaryChanged => {
627 cx.emit(EditorEvent::TitleChanged);
628 self.text_thread.update(cx, |text_thread, cx| {
629 text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
630 });
631 }
632 TextThreadEvent::SummaryGenerated => {}
633 TextThreadEvent::PathChanged { .. } => {}
634 TextThreadEvent::StartedThoughtProcess(range) => {
635 let creases = self.insert_thought_process_output_sections(
636 [(
637 ThoughtProcessOutputSection {
638 range: range.clone(),
639 },
640 ThoughtProcessStatus::Pending,
641 )],
642 window,
643 cx,
644 );
645 self.pending_thought_process = Some((creases[0], range.start));
646 }
647 TextThreadEvent::EndedThoughtProcess(end) => {
648 if let Some((crease_id, start)) = self.pending_thought_process.take() {
649 self.editor.update(cx, |editor, cx| {
650 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
651 let start_anchor =
652 multi_buffer_snapshot.as_singleton_anchor(start).unwrap();
653
654 editor.display_map.update(cx, |display_map, cx| {
655 display_map.unfold_intersecting(
656 vec![start_anchor..start_anchor],
657 true,
658 cx,
659 );
660 });
661 editor.remove_creases(vec![crease_id], cx);
662 });
663 self.insert_thought_process_output_sections(
664 [(
665 ThoughtProcessOutputSection { range: start..*end },
666 ThoughtProcessStatus::Completed,
667 )],
668 window,
669 cx,
670 );
671 }
672 }
673 TextThreadEvent::StreamedCompletion => {
674 self.editor.update(cx, |editor, cx| {
675 if let Some(scroll_position) = self.scroll_position {
676 let snapshot = editor.snapshot(window, cx);
677 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
678 let scroll_top =
679 cursor_point.row().as_f64() - scroll_position.offset_before_cursor.y;
680 editor.set_scroll_position(
681 point(scroll_position.offset_before_cursor.x, scroll_top),
682 window,
683 cx,
684 );
685 }
686 });
687 }
688 TextThreadEvent::ParsedSlashCommandsUpdated { removed, updated } => {
689 self.editor.update(cx, |editor, cx| {
690 let buffer = editor.buffer().read(cx).snapshot(cx);
691 let (excerpt_id, _, _) = buffer.as_singleton().unwrap();
692
693 editor.remove_creases(
694 removed
695 .iter()
696 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
697 cx,
698 );
699
700 let crease_ids = editor.insert_creases(
701 updated.iter().map(|command| {
702 let workspace = self.workspace.clone();
703 let confirm_command = Arc::new({
704 let text_thread_editor = text_thread_editor.clone();
705 let command = command.clone();
706 move |window: &mut Window, cx: &mut App| {
707 text_thread_editor
708 .update(cx, |text_thread_editor, cx| {
709 text_thread_editor.run_command(
710 command.source_range.clone(),
711 &command.name,
712 &command.arguments,
713 false,
714 workspace.clone(),
715 window,
716 cx,
717 );
718 })
719 .ok();
720 }
721 });
722 let placeholder = FoldPlaceholder {
723 render: Arc::new(move |_, _, _| Empty.into_any()),
724 ..Default::default()
725 };
726 let render_toggle = {
727 let confirm_command = confirm_command.clone();
728 let command = command.clone();
729 move |row, _, _, _window: &mut Window, _cx: &mut App| {
730 render_pending_slash_command_gutter_decoration(
731 row,
732 &command.status,
733 confirm_command.clone(),
734 )
735 }
736 };
737 let render_trailer = {
738 move |_row, _unfold, _window: &mut Window, _cx: &mut App| {
739 Empty.into_any()
740 }
741 };
742
743 let range = buffer
744 .anchor_range_in_excerpt(excerpt_id, command.source_range.clone())
745 .unwrap();
746 Crease::inline(range, placeholder, render_toggle, render_trailer)
747 }),
748 cx,
749 );
750
751 self.pending_slash_command_creases.extend(
752 updated
753 .iter()
754 .map(|command| command.source_range.clone())
755 .zip(crease_ids),
756 );
757 })
758 }
759 TextThreadEvent::InvokedSlashCommandChanged { command_id } => {
760 self.update_invoked_slash_command(*command_id, window, cx);
761 }
762 TextThreadEvent::SlashCommandOutputSectionAdded { section } => {
763 self.insert_slash_command_output_sections([section.clone()], false, window, cx);
764 }
765 TextThreadEvent::Operation(_) => {}
766 TextThreadEvent::ShowAssistError(error_message) => {
767 self.last_error = Some(AssistError::Message(error_message.clone()));
768 }
769 TextThreadEvent::ShowPaymentRequiredError => {
770 self.last_error = Some(AssistError::PaymentRequired);
771 }
772 }
773 }
774
775 fn update_invoked_slash_command(
776 &mut self,
777 command_id: InvokedSlashCommandId,
778 window: &mut Window,
779 cx: &mut Context<Self>,
780 ) {
781 if let Some(invoked_slash_command) =
782 self.text_thread.read(cx).invoked_slash_command(&command_id)
783 && let InvokedSlashCommandStatus::Finished = invoked_slash_command.status
784 {
785 let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone();
786 for range in run_commands_in_ranges {
787 let commands = self.text_thread.update(cx, |text_thread, cx| {
788 text_thread.reparse(cx);
789 text_thread
790 .pending_commands_for_range(range.clone(), cx)
791 .to_vec()
792 });
793
794 for command in commands {
795 self.run_command(
796 command.source_range,
797 &command.name,
798 &command.arguments,
799 false,
800 self.workspace.clone(),
801 window,
802 cx,
803 );
804 }
805 }
806 }
807
808 self.editor.update(cx, |editor, cx| {
809 if let Some(invoked_slash_command) =
810 self.text_thread.read(cx).invoked_slash_command(&command_id)
811 {
812 if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
813 let buffer = editor.buffer().read(cx).snapshot(cx);
814 let (excerpt_id, _buffer_id, _buffer_snapshot) = buffer.as_singleton().unwrap();
815
816 let range = buffer
817 .anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone())
818 .unwrap();
819 editor.remove_folds_with_type(
820 &[range],
821 TypeId::of::<PendingSlashCommand>(),
822 false,
823 cx,
824 );
825
826 editor.remove_creases(
827 HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
828 cx,
829 );
830 } else if let hash_map::Entry::Vacant(entry) =
831 self.invoked_slash_command_creases.entry(command_id)
832 {
833 let buffer = editor.buffer().read(cx).snapshot(cx);
834 let (excerpt_id, _buffer_id, _buffer_snapshot) = buffer.as_singleton().unwrap();
835 let context = self.text_thread.downgrade();
836 let range = buffer
837 .anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone())
838 .unwrap();
839 let crease = Crease::inline(
840 range,
841 invoked_slash_command_fold_placeholder(command_id, context),
842 fold_toggle("invoked-slash-command"),
843 |_row, _folded, _window, _cx| Empty.into_any(),
844 );
845 let crease_ids = editor.insert_creases([crease.clone()], cx);
846 editor.fold_creases(vec![crease], false, window, cx);
847 entry.insert(crease_ids[0]);
848 } else {
849 cx.notify()
850 }
851 } else {
852 editor.remove_creases(
853 HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
854 cx,
855 );
856 cx.notify();
857 };
858 });
859 }
860
861 fn insert_thought_process_output_sections(
862 &mut self,
863 sections: impl IntoIterator<
864 Item = (
865 ThoughtProcessOutputSection<language::Anchor>,
866 ThoughtProcessStatus,
867 ),
868 >,
869 window: &mut Window,
870 cx: &mut Context<Self>,
871 ) -> Vec<CreaseId> {
872 self.editor.update(cx, |editor, cx| {
873 let buffer = editor.buffer().read(cx).snapshot(cx);
874 let excerpt_id = buffer.as_singleton().unwrap().0;
875 let mut buffer_rows_to_fold = BTreeSet::new();
876 let mut creases = Vec::new();
877 for (section, status) in sections {
878 let range = buffer
879 .anchor_range_in_excerpt(excerpt_id, section.range)
880 .unwrap();
881 let buffer_row = MultiBufferRow(range.start.to_point(&buffer).row);
882 buffer_rows_to_fold.insert(buffer_row);
883 creases.push(
884 Crease::inline(
885 range,
886 FoldPlaceholder {
887 render: render_thought_process_fold_icon_button(
888 cx.entity().downgrade(),
889 status,
890 ),
891 merge_adjacent: false,
892 ..Default::default()
893 },
894 render_slash_command_output_toggle,
895 |_, _, _, _| Empty.into_any_element(),
896 )
897 .with_metadata(CreaseMetadata {
898 icon_path: SharedString::from(IconName::ZedAgent.path()),
899 label: "Thinking Process".into(),
900 }),
901 );
902 }
903
904 let creases = editor.insert_creases(creases, cx);
905
906 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
907 editor.fold_at(buffer_row, window, cx);
908 }
909
910 creases
911 })
912 }
913
914 fn insert_slash_command_output_sections(
915 &mut self,
916 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
917 expand_result: bool,
918 window: &mut Window,
919 cx: &mut Context<Self>,
920 ) {
921 self.editor.update(cx, |editor, cx| {
922 let buffer = editor.buffer().read(cx).snapshot(cx);
923 let excerpt_id = buffer.as_singleton().unwrap().0;
924 let mut buffer_rows_to_fold = BTreeSet::new();
925 let mut creases = Vec::new();
926 for section in sections {
927 let range = buffer
928 .anchor_range_in_excerpt(excerpt_id, section.range)
929 .unwrap();
930 let buffer_row = MultiBufferRow(range.start.to_point(&buffer).row);
931 buffer_rows_to_fold.insert(buffer_row);
932 creases.push(
933 Crease::inline(
934 range,
935 FoldPlaceholder {
936 render: render_fold_icon_button(
937 cx.entity().downgrade(),
938 section.icon.path().into(),
939 section.label.clone(),
940 ),
941 merge_adjacent: false,
942 ..Default::default()
943 },
944 render_slash_command_output_toggle,
945 |_, _, _, _| Empty.into_any_element(),
946 )
947 .with_metadata(CreaseMetadata {
948 icon_path: section.icon.path().into(),
949 label: section.label,
950 }),
951 );
952 }
953
954 editor.insert_creases(creases, cx);
955
956 if expand_result {
957 buffer_rows_to_fold.clear();
958 }
959 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
960 editor.fold_at(buffer_row, window, cx);
961 }
962 });
963 }
964
965 fn handle_editor_event(
966 &mut self,
967 _: &Entity<Editor>,
968 event: &EditorEvent,
969 window: &mut Window,
970 cx: &mut Context<Self>,
971 ) {
972 match event {
973 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
974 let cursor_scroll_position = self.cursor_scroll_position(window, cx);
975 if *autoscroll {
976 self.scroll_position = cursor_scroll_position;
977 } else if self.scroll_position != cursor_scroll_position {
978 self.scroll_position = None;
979 }
980 }
981 EditorEvent::SelectionsChanged { .. } => {
982 self.scroll_position = self.cursor_scroll_position(window, cx);
983 }
984 _ => {}
985 }
986 cx.emit(event.clone());
987 }
988
989 fn handle_editor_search_event(
990 &mut self,
991 _: &Entity<Editor>,
992 event: &SearchEvent,
993 _window: &mut Window,
994 cx: &mut Context<Self>,
995 ) {
996 cx.emit(event.clone());
997 }
998
999 fn cursor_scroll_position(
1000 &self,
1001 window: &mut Window,
1002 cx: &mut Context<Self>,
1003 ) -> Option<ScrollPosition> {
1004 self.editor.update(cx, |editor, cx| {
1005 let snapshot = editor.snapshot(window, cx);
1006 let cursor = editor.selections.newest_anchor().head();
1007 let cursor_row = cursor
1008 .to_display_point(&snapshot.display_snapshot)
1009 .row()
1010 .as_f64();
1011 let scroll_position = editor
1012 .scroll_manager
1013 .scroll_position(&snapshot.display_snapshot, cx);
1014
1015 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
1016 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
1017 Some(ScrollPosition {
1018 cursor,
1019 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
1020 })
1021 } else {
1022 None
1023 }
1024 })
1025 }
1026
1027 fn esc_kbd(cx: &App) -> Div {
1028 let colors = cx.theme().colors().clone();
1029
1030 h_flex()
1031 .items_center()
1032 .gap_1()
1033 .font(
1034 theme_settings::ThemeSettings::get_global(cx)
1035 .buffer_font
1036 .clone(),
1037 )
1038 .text_size(TextSize::XSmall.rems(cx))
1039 .text_color(colors.text_muted)
1040 .child("Press")
1041 .child(
1042 h_flex()
1043 .rounded_sm()
1044 .px_1()
1045 .mr_0p5()
1046 .border_1()
1047 .border_color(colors.border_variant.alpha(0.6))
1048 .bg(colors.element_background.alpha(0.6))
1049 .child("esc"),
1050 )
1051 .child("to cancel")
1052 }
1053
1054 fn update_message_headers(&mut self, cx: &mut Context<Self>) {
1055 self.editor.update(cx, |editor, cx| {
1056 let buffer = editor.buffer().read(cx).snapshot(cx);
1057
1058 let excerpt_id = buffer.as_singleton().unwrap().0;
1059 let mut old_blocks = std::mem::take(&mut self.blocks);
1060 let mut blocks_to_remove: HashMap<_, _> = old_blocks
1061 .iter()
1062 .map(|(message_id, (_, block_id))| (*message_id, *block_id))
1063 .collect();
1064 let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default();
1065
1066 let render_block = |message: MessageMetadata| -> RenderBlock {
1067 Arc::new({
1068 let text_thread = self.text_thread.clone();
1069
1070 move |cx| {
1071 let message_id = MessageId(message.timestamp);
1072 let llm_loading = message.role == Role::Assistant
1073 && message.status == MessageStatus::Pending;
1074
1075 let (label, spinner, note) = match message.role {
1076 Role::User => (
1077 Label::new("You").color(Color::Default).into_any_element(),
1078 None,
1079 None,
1080 ),
1081 Role::Assistant => {
1082 let base_label = Label::new("Agent").color(Color::Info);
1083 let mut spinner = None;
1084 let mut note = None;
1085 let animated_label = if llm_loading {
1086 base_label
1087 .with_animation(
1088 "pulsating-label",
1089 Animation::new(Duration::from_secs(2))
1090 .repeat()
1091 .with_easing(pulsating_between(0.4, 0.8)),
1092 |label, delta| label.alpha(delta),
1093 )
1094 .into_any_element()
1095 } else {
1096 base_label.into_any_element()
1097 };
1098 if llm_loading {
1099 spinner = Some(
1100 Icon::new(IconName::ArrowCircle)
1101 .size(IconSize::XSmall)
1102 .color(Color::Info)
1103 .with_rotate_animation(2)
1104 .into_any_element(),
1105 );
1106 note = Some(Self::esc_kbd(cx).into_any_element());
1107 }
1108 (animated_label, spinner, note)
1109 }
1110 Role::System => (
1111 Label::new("System")
1112 .color(Color::Warning)
1113 .into_any_element(),
1114 None,
1115 None,
1116 ),
1117 };
1118
1119 let sender = h_flex()
1120 .items_center()
1121 .gap_2p5()
1122 .child(
1123 ButtonLike::new("role")
1124 .style(ButtonStyle::Filled)
1125 .child(
1126 h_flex()
1127 .items_center()
1128 .gap_1p5()
1129 .child(label)
1130 .children(spinner),
1131 )
1132 .tooltip(|_window, cx| {
1133 Tooltip::with_meta(
1134 "Toggle message role",
1135 None,
1136 "Available roles: You (User), Agent, System",
1137 cx,
1138 )
1139 })
1140 .on_click({
1141 let text_thread = text_thread.clone();
1142 move |_, _window, cx| {
1143 text_thread.update(cx, |text_thread, cx| {
1144 text_thread.cycle_message_roles(
1145 HashSet::from_iter(Some(message_id)),
1146 cx,
1147 )
1148 })
1149 }
1150 }),
1151 )
1152 .children(note);
1153
1154 h_flex()
1155 .id(("message_header", message_id.as_u64()))
1156 .pl(cx.margins.gutter.full_width())
1157 .h_11()
1158 .w_full()
1159 .relative()
1160 .gap_1p5()
1161 .child(sender)
1162 .children(match &message.cache {
1163 Some(cache) if cache.is_final_anchor => match cache.status {
1164 CacheStatus::Cached => Some(
1165 div()
1166 .id("cached")
1167 .child(
1168 Icon::new(IconName::DatabaseZap)
1169 .size(IconSize::XSmall)
1170 .color(Color::Hint),
1171 )
1172 .tooltip(|_window, cx| {
1173 Tooltip::with_meta(
1174 "Context Cached",
1175 None,
1176 "Large messages cached to optimize performance",
1177 cx,
1178 )
1179 })
1180 .into_any_element(),
1181 ),
1182 CacheStatus::Pending => Some(
1183 div()
1184 .child(
1185 Icon::new(IconName::Ellipsis)
1186 .size(IconSize::XSmall)
1187 .color(Color::Hint),
1188 )
1189 .into_any_element(),
1190 ),
1191 },
1192 _ => None,
1193 })
1194 .children(match &message.status {
1195 MessageStatus::Error(error) => Some(
1196 Button::new("show-error", "Error")
1197 .color(Color::Error)
1198 .selected_label_color(Color::Error)
1199 .start_icon(
1200 Icon::new(IconName::XCircle)
1201 .size(IconSize::XSmall)
1202 .color(Color::Error),
1203 )
1204 .tooltip(Tooltip::text("View Details"))
1205 .on_click({
1206 let text_thread = text_thread.clone();
1207 let error = error.clone();
1208 move |_, _window, cx| {
1209 text_thread.update(cx, |_, cx| {
1210 cx.emit(TextThreadEvent::ShowAssistError(
1211 error.clone(),
1212 ));
1213 });
1214 }
1215 })
1216 .into_any_element(),
1217 ),
1218 MessageStatus::Canceled => Some(
1219 h_flex()
1220 .gap_1()
1221 .items_center()
1222 .child(
1223 Icon::new(IconName::XCircle)
1224 .color(Color::Disabled)
1225 .size(IconSize::XSmall),
1226 )
1227 .child(
1228 Label::new("Canceled")
1229 .size(LabelSize::Small)
1230 .color(Color::Disabled),
1231 )
1232 .into_any_element(),
1233 ),
1234 _ => None,
1235 })
1236 .into_any_element()
1237 }
1238 })
1239 };
1240 let create_block_properties = |message: &Message| BlockProperties {
1241 height: Some(2),
1242 style: BlockStyle::Sticky,
1243 placement: BlockPlacement::Above(
1244 buffer
1245 .anchor_in_excerpt(excerpt_id, message.anchor_range.start)
1246 .unwrap(),
1247 ),
1248 priority: usize::MAX,
1249 render: render_block(MessageMetadata::from(message)),
1250 };
1251 let mut new_blocks = vec![];
1252 let mut block_index_to_message = vec![];
1253 for message in self.text_thread.read(cx).messages(cx) {
1254 if blocks_to_remove.remove(&message.id).is_some() {
1255 // This is an old message that we might modify.
1256 let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
1257 debug_assert!(
1258 false,
1259 "old_blocks should contain a message_id we've just removed."
1260 );
1261 continue;
1262 };
1263 // Should we modify it?
1264 let message_meta = MessageMetadata::from(&message);
1265 if meta != &message_meta {
1266 blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
1267 *meta = message_meta;
1268 }
1269 } else {
1270 // This is a new message.
1271 new_blocks.push(create_block_properties(&message));
1272 block_index_to_message.push((message.id, MessageMetadata::from(&message)));
1273 }
1274 }
1275 editor.replace_blocks(blocks_to_replace, None, cx);
1276 editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx);
1277
1278 let ids = editor.insert_blocks(new_blocks, None, cx);
1279 old_blocks.extend(ids.into_iter().zip(block_index_to_message).map(
1280 |(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)),
1281 ));
1282 self.blocks = old_blocks;
1283 });
1284 }
1285
1286 /// Returns either the selected text, or the content of the Markdown code
1287 /// block surrounding the cursor.
1288 fn get_selection_or_code_block(
1289 context_editor_view: &Entity<TextThreadEditor>,
1290 cx: &mut Context<Workspace>,
1291 ) -> Option<(String, bool)> {
1292 const CODE_FENCE_DELIMITER: &str = "```";
1293
1294 let text_thread_editor = context_editor_view.read(cx).editor.clone();
1295 text_thread_editor.update(cx, |text_thread_editor, cx| {
1296 let display_map = text_thread_editor.display_snapshot(cx);
1297 if text_thread_editor
1298 .selections
1299 .newest::<Point>(&display_map)
1300 .is_empty()
1301 {
1302 let snapshot = text_thread_editor.buffer().read(cx).snapshot(cx);
1303 let (_, _, snapshot) = snapshot.as_singleton()?;
1304
1305 let head = text_thread_editor
1306 .selections
1307 .newest::<Point>(&display_map)
1308 .head();
1309 let offset = snapshot.point_to_offset(head);
1310
1311 let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
1312 let mut text = snapshot
1313 .text_for_range(surrounding_code_block_range)
1314 .collect::<String>();
1315
1316 // If there is no newline trailing the closing three-backticks, then
1317 // tree-sitter-md extends the range of the content node to include
1318 // the backticks.
1319 if text.ends_with(CODE_FENCE_DELIMITER) {
1320 text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
1321 }
1322
1323 (!text.is_empty()).then_some((text, true))
1324 } else {
1325 let selection = text_thread_editor.selections.newest_adjusted(&display_map);
1326 let buffer = text_thread_editor.buffer().read(cx).snapshot(cx);
1327 let selected_text = buffer.text_for_range(selection.range()).collect::<String>();
1328
1329 (!selected_text.is_empty()).then_some((selected_text, false))
1330 }
1331 })
1332 }
1333
1334 pub fn insert_selection(
1335 workspace: &mut Workspace,
1336 _: &InsertIntoEditor,
1337 window: &mut Window,
1338 cx: &mut Context<Workspace>,
1339 ) {
1340 let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
1341 return;
1342 };
1343 let Some(context_editor_view) =
1344 agent_panel_delegate.active_text_thread_editor(workspace, window, cx)
1345 else {
1346 return;
1347 };
1348 let Some(active_editor_view) = workspace
1349 .active_item(cx)
1350 .and_then(|item| item.act_as::<Editor>(cx))
1351 else {
1352 return;
1353 };
1354
1355 if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
1356 active_editor_view.update(cx, |editor, cx| {
1357 editor.insert(&text, window, cx);
1358 editor.focus_handle(cx).focus(window, cx);
1359 })
1360 }
1361 }
1362
1363 pub fn copy_code(
1364 workspace: &mut Workspace,
1365 _: &CopyCode,
1366 window: &mut Window,
1367 cx: &mut Context<Workspace>,
1368 ) {
1369 let result = maybe!({
1370 let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
1371 let context_editor_view =
1372 agent_panel_delegate.active_text_thread_editor(workspace, window, cx)?;
1373 Self::get_selection_or_code_block(&context_editor_view, cx)
1374 });
1375 let Some((text, is_code_block)) = result else {
1376 return;
1377 };
1378
1379 cx.write_to_clipboard(ClipboardItem::new_string(text));
1380
1381 struct CopyToClipboardToast;
1382 workspace.show_toast(
1383 Toast::new(
1384 NotificationId::unique::<CopyToClipboardToast>(),
1385 format!(
1386 "{} copied to clipboard.",
1387 if is_code_block {
1388 "Code block"
1389 } else {
1390 "Selection"
1391 }
1392 ),
1393 )
1394 .autohide(),
1395 cx,
1396 );
1397 }
1398
1399 pub fn handle_insert_dragged_files(
1400 workspace: &mut Workspace,
1401 action: &InsertDraggedFiles,
1402 window: &mut Window,
1403 cx: &mut Context<Workspace>,
1404 ) {
1405 let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
1406 return;
1407 };
1408 let Some(context_editor_view) =
1409 agent_panel_delegate.active_text_thread_editor(workspace, window, cx)
1410 else {
1411 return;
1412 };
1413
1414 let project = context_editor_view.read(cx).project.clone();
1415
1416 let paths = match action {
1417 InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
1418 InsertDraggedFiles::ExternalFiles(paths) => {
1419 let tasks = paths
1420 .clone()
1421 .into_iter()
1422 .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
1423 .collect::<Vec<_>>();
1424
1425 cx.background_spawn(async move {
1426 let mut paths = vec![];
1427 let mut worktrees = vec![];
1428
1429 let opened_paths = futures::future::join_all(tasks).await;
1430
1431 for entry in opened_paths {
1432 if let Some((worktree, project_path)) = entry.log_err() {
1433 worktrees.push(worktree);
1434 paths.push(project_path);
1435 }
1436 }
1437
1438 (paths, worktrees)
1439 })
1440 }
1441 };
1442
1443 context_editor_view.update(cx, |_, cx| {
1444 cx.spawn_in(window, async move |this, cx| {
1445 let (paths, dragged_file_worktrees) = paths.await;
1446 this.update_in(cx, |this, window, cx| {
1447 this.insert_dragged_files(paths, dragged_file_worktrees, window, cx);
1448 })
1449 .ok();
1450 })
1451 .detach();
1452 })
1453 }
1454
1455 pub fn insert_dragged_files(
1456 &mut self,
1457 opened_paths: Vec<ProjectPath>,
1458 added_worktrees: Vec<Entity<Worktree>>,
1459 window: &mut Window,
1460 cx: &mut Context<Self>,
1461 ) {
1462 let mut file_slash_command_args = vec![];
1463 for project_path in opened_paths.into_iter() {
1464 let Some(worktree) = self
1465 .project
1466 .read(cx)
1467 .worktree_for_id(project_path.worktree_id, cx)
1468 else {
1469 continue;
1470 };
1471 let path_style = worktree.read(cx).path_style();
1472 let full_path = worktree
1473 .read(cx)
1474 .root_name()
1475 .join(&project_path.path)
1476 .display(path_style)
1477 .into_owned();
1478 file_slash_command_args.push(full_path);
1479 }
1480
1481 let cmd_name = FileSlashCommand.name();
1482
1483 let file_argument = file_slash_command_args.join(" ");
1484
1485 self.editor.update(cx, |editor, cx| {
1486 editor.insert("\n", window, cx);
1487 editor.insert(&format!("/{} {}", cmd_name, file_argument), window, cx);
1488 });
1489 self.confirm_command(&ConfirmCommand, window, cx);
1490 self.dragged_file_worktrees.extend(added_worktrees);
1491 }
1492
1493 pub fn quote_selection(
1494 workspace: &mut Workspace,
1495 _: &AddSelectionToThread,
1496 window: &mut Window,
1497 cx: &mut Context<Workspace>,
1498 ) {
1499 let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
1500 return;
1501 };
1502
1503 // Get buffer info for the delegate call (even if empty, ThreadView ignores these
1504 // params and calls insert_selections which handles both terminal and buffer)
1505 if let Some((selections, buffer)) = maybe!({
1506 let editor = workspace
1507 .active_item(cx)
1508 .and_then(|item| item.act_as::<Editor>(cx))?;
1509
1510 let buffer = editor.read(cx).buffer().clone();
1511 let snapshot = buffer.read(cx).snapshot(cx);
1512 let selections = editor.update(cx, |editor, cx| {
1513 editor
1514 .selections
1515 .all_adjusted(&editor.display_snapshot(cx))
1516 .into_iter()
1517 .map(|s| {
1518 let (start, end) = if s.is_empty() {
1519 let row = multi_buffer::MultiBufferRow(s.start.row);
1520 let line_start = text::Point::new(s.start.row, 0);
1521 let line_end = text::Point::new(s.start.row, snapshot.line_len(row));
1522 (line_start, line_end)
1523 } else {
1524 (s.start, s.end)
1525 };
1526 snapshot.anchor_after(start)..snapshot.anchor_before(end)
1527 })
1528 .collect::<Vec<_>>()
1529 });
1530 Some((selections, buffer))
1531 }) {
1532 agent_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
1533 }
1534 }
1535
1536 pub fn quote_ranges(
1537 &mut self,
1538 ranges: Vec<Range<Point>>,
1539 snapshot: MultiBufferSnapshot,
1540 window: &mut Window,
1541 cx: &mut Context<Self>,
1542 ) {
1543 let creases = selections_creases(ranges, snapshot, cx);
1544
1545 self.editor.update(cx, |editor, cx| {
1546 editor.insert("\n", window, cx);
1547 for (text, crease_title) in creases {
1548 let point = editor
1549 .selections
1550 .newest::<Point>(&editor.display_snapshot(cx))
1551 .head();
1552 let start_row = MultiBufferRow(point.row);
1553
1554 editor.insert(&text, window, cx);
1555
1556 let snapshot = editor.buffer().read(cx).snapshot(cx);
1557 let anchor_before = snapshot.anchor_after(point);
1558 let anchor_after = editor
1559 .selections
1560 .newest_anchor()
1561 .head()
1562 .bias_left(&snapshot);
1563
1564 editor.insert("\n", window, cx);
1565
1566 let fold_placeholder =
1567 quote_selection_fold_placeholder(crease_title, cx.entity().downgrade());
1568 let crease = Crease::inline(
1569 anchor_before..anchor_after,
1570 fold_placeholder,
1571 render_quote_selection_output_toggle,
1572 |_, _, _, _| Empty.into_any(),
1573 );
1574 editor.insert_creases(vec![crease], cx);
1575 editor.fold_at(start_row, window, cx);
1576 }
1577 })
1578 }
1579
1580 pub fn quote_terminal_text(
1581 &mut self,
1582 text: String,
1583 window: &mut Window,
1584 cx: &mut Context<Self>,
1585 ) {
1586 let crease_title = "terminal".to_string();
1587 let formatted_text = format!("```console\n{}\n```\n", text);
1588
1589 self.editor.update(cx, |editor, cx| {
1590 // Insert newline first if not at the start of a line
1591 let point = editor
1592 .selections
1593 .newest::<Point>(&editor.display_snapshot(cx))
1594 .head();
1595 if point.column > 0 {
1596 editor.insert("\n", window, cx);
1597 }
1598
1599 let point = editor
1600 .selections
1601 .newest::<Point>(&editor.display_snapshot(cx))
1602 .head();
1603 let start_row = MultiBufferRow(point.row);
1604
1605 editor.insert(&formatted_text, window, cx);
1606
1607 let snapshot = editor.buffer().read(cx).snapshot(cx);
1608 let anchor_before = snapshot.anchor_after(point);
1609 let anchor_after = editor
1610 .selections
1611 .newest_anchor()
1612 .head()
1613 .bias_left(&snapshot);
1614
1615 let fold_placeholder =
1616 quote_selection_fold_placeholder(crease_title, cx.entity().downgrade());
1617 let crease = Crease::inline(
1618 anchor_before..anchor_after,
1619 fold_placeholder,
1620 render_quote_selection_output_toggle,
1621 |_, _, _, _| Empty.into_any(),
1622 );
1623 editor.insert_creases(vec![crease], cx);
1624 editor.fold_at(start_row, window, cx);
1625 })
1626 }
1627
1628 fn copy(&mut self, _: &editor::actions::Copy, _window: &mut Window, cx: &mut Context<Self>) {
1629 if self.editor.read(cx).selections.count() == 1 {
1630 let (copied_text, metadata, _) = self.get_clipboard_contents(cx);
1631 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
1632 copied_text,
1633 metadata,
1634 ));
1635 cx.stop_propagation();
1636 return;
1637 }
1638
1639 cx.propagate();
1640 }
1641
1642 fn cut(&mut self, _: &editor::actions::Cut, window: &mut Window, cx: &mut Context<Self>) {
1643 if self.editor.read(cx).selections.count() == 1 {
1644 let (copied_text, metadata, selections) = self.get_clipboard_contents(cx);
1645
1646 self.editor.update(cx, |editor, cx| {
1647 editor.transact(window, cx, |this, window, cx| {
1648 this.change_selections(Default::default(), window, cx, |s| {
1649 s.select(selections);
1650 });
1651 this.insert("", window, cx);
1652 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
1653 copied_text,
1654 metadata,
1655 ));
1656 });
1657 });
1658
1659 cx.stop_propagation();
1660 return;
1661 }
1662
1663 cx.propagate();
1664 }
1665
1666 fn get_clipboard_contents(
1667 &mut self,
1668 cx: &mut Context<Self>,
1669 ) -> (
1670 String,
1671 CopyMetadata,
1672 Vec<text::Selection<MultiBufferOffset>>,
1673 ) {
1674 let (mut selection, creases) = self.editor.update(cx, |editor, cx| {
1675 let mut selection = editor
1676 .selections
1677 .newest_adjusted(&editor.display_snapshot(cx));
1678 let snapshot = editor.buffer().read(cx).snapshot(cx);
1679
1680 selection.goal = SelectionGoal::None;
1681
1682 let selection_start = snapshot.point_to_offset(selection.start);
1683
1684 (
1685 selection.map(|point| snapshot.point_to_offset(point)),
1686 editor.display_map.update(cx, |display_map, cx| {
1687 display_map
1688 .snapshot(cx)
1689 .crease_snapshot
1690 .creases_in_range(
1691 MultiBufferRow(selection.start.row)
1692 ..MultiBufferRow(selection.end.row + 1),
1693 &snapshot,
1694 )
1695 .filter_map(|crease| {
1696 if let Crease::Inline {
1697 range, metadata, ..
1698 } = &crease
1699 {
1700 let metadata = metadata.as_ref()?;
1701 let start = range
1702 .start
1703 .to_offset(&snapshot)
1704 .saturating_sub(selection_start);
1705 let end = range
1706 .end
1707 .to_offset(&snapshot)
1708 .saturating_sub(selection_start);
1709
1710 let range_relative_to_selection = start..end;
1711 if !range_relative_to_selection.is_empty() {
1712 return Some(SelectedCreaseMetadata {
1713 range_relative_to_selection,
1714 crease: metadata.clone(),
1715 });
1716 }
1717 }
1718 None
1719 })
1720 .collect::<Vec<_>>()
1721 }),
1722 )
1723 });
1724
1725 let text_thread = self.text_thread.read(cx);
1726
1727 let mut text = String::new();
1728
1729 // If selection is empty, we want to copy the entire line
1730 if selection.range().is_empty() {
1731 let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
1732 let point = snapshot.offset_to_point(selection.range().start);
1733 selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
1734 selection.end = snapshot
1735 .point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point()));
1736 for chunk in snapshot.text_for_range(selection.range()) {
1737 text.push_str(chunk);
1738 }
1739 } else {
1740 for message in text_thread.messages(cx) {
1741 if message.offset_range.start >= selection.range().end.0 {
1742 break;
1743 } else if message.offset_range.end >= selection.range().start.0 {
1744 let range = cmp::max(message.offset_range.start, selection.range().start.0)
1745 ..cmp::min(message.offset_range.end, selection.range().end.0);
1746 if !range.is_empty() {
1747 for chunk in text_thread.buffer().read(cx).text_for_range(range) {
1748 text.push_str(chunk);
1749 }
1750 if message.offset_range.end < selection.range().end.0 {
1751 text.push('\n');
1752 }
1753 }
1754 }
1755 }
1756 }
1757 (text, CopyMetadata { creases }, vec![selection])
1758 }
1759
1760 fn paste(
1761 &mut self,
1762 action: &editor::actions::Paste,
1763 window: &mut Window,
1764 cx: &mut Context<Self>,
1765 ) {
1766 let Some(workspace) = self.workspace.upgrade() else {
1767 return;
1768 };
1769 let editor_clipboard_selections = cx.read_from_clipboard().and_then(|item| {
1770 item.entries().iter().find_map(|entry| match entry {
1771 ClipboardEntry::String(text) => {
1772 text.metadata_json::<Vec<editor::ClipboardSelection>>()
1773 }
1774 _ => None,
1775 })
1776 });
1777
1778 // Insert creases for pasted clipboard selections that:
1779 // 1. Contain exactly one selection
1780 // 2. Have an associated file path
1781 // 3. Span multiple lines (not single-line selections)
1782 // 4. Belong to a file that exists in the current project
1783 let should_insert_creases = util::maybe!({
1784 let selections = editor_clipboard_selections.as_ref()?;
1785 if selections.len() > 1 {
1786 return Some(false);
1787 }
1788 let selection = selections.first()?;
1789 let file_path = selection.file_path.as_ref()?;
1790 let line_range = selection.line_range.as_ref()?;
1791
1792 if line_range.start() == line_range.end() {
1793 return Some(false);
1794 }
1795
1796 Some(
1797 workspace
1798 .read(cx)
1799 .project()
1800 .read(cx)
1801 .project_path_for_absolute_path(file_path, cx)
1802 .is_some(),
1803 )
1804 })
1805 .unwrap_or(false);
1806
1807 if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
1808 let clipboard_text = clipboard_item
1809 .entries()
1810 .iter()
1811 .find_map(|entry| match entry {
1812 ClipboardEntry::String(s) => Some(s),
1813 _ => None,
1814 });
1815 if let Some(clipboard_text) = clipboard_text {
1816 if let Some(selections) = editor_clipboard_selections {
1817 cx.stop_propagation();
1818
1819 let text = clipboard_text.text();
1820 self.editor.update(cx, |editor, cx| {
1821 let mut current_offset = 0;
1822 let weak_editor = cx.entity().downgrade();
1823
1824 for selection in selections {
1825 if let (Some(file_path), Some(line_range)) =
1826 (selection.file_path, selection.line_range)
1827 {
1828 let selected_text =
1829 &text[current_offset..current_offset + selection.len];
1830 let fence = assistant_slash_commands::codeblock_fence_for_path(
1831 file_path.to_str(),
1832 Some(line_range.clone()),
1833 );
1834 let formatted_text = format!("{fence}{selected_text}\n```");
1835
1836 let insert_point = editor
1837 .selections
1838 .newest::<Point>(&editor.display_snapshot(cx))
1839 .head();
1840 let start_row = MultiBufferRow(insert_point.row);
1841
1842 editor.insert(&formatted_text, window, cx);
1843
1844 let snapshot = editor.buffer().read(cx).snapshot(cx);
1845 let anchor_before = snapshot.anchor_after(insert_point);
1846 let anchor_after = editor
1847 .selections
1848 .newest_anchor()
1849 .head()
1850 .bias_left(&snapshot);
1851
1852 editor.insert("\n", window, cx);
1853
1854 let crease_text = acp_thread::selection_name(
1855 Some(file_path.as_ref()),
1856 &line_range,
1857 );
1858
1859 let fold_placeholder = quote_selection_fold_placeholder(
1860 crease_text,
1861 weak_editor.clone(),
1862 );
1863 let crease = Crease::inline(
1864 anchor_before..anchor_after,
1865 fold_placeholder,
1866 render_quote_selection_output_toggle,
1867 |_, _, _, _| Empty.into_any(),
1868 );
1869 editor.insert_creases(vec![crease], cx);
1870 editor.fold_at(start_row, window, cx);
1871
1872 current_offset += selection.len;
1873 if !selection.is_entire_line && current_offset < text.len() {
1874 current_offset += 1;
1875 }
1876 }
1877 }
1878 });
1879 return;
1880 }
1881 }
1882 }
1883
1884 cx.stop_propagation();
1885
1886 let clipboard_item = cx.read_from_clipboard();
1887
1888 let mut images: Vec<gpui::Image> = Vec::new();
1889 let mut paths: Vec<std::path::PathBuf> = Vec::new();
1890 let mut metadata: Option<CopyMetadata> = None;
1891
1892 if let Some(item) = &clipboard_item {
1893 for entry in item.entries() {
1894 match entry {
1895 ClipboardEntry::Image(image) => images.push(image.clone()),
1896 ClipboardEntry::ExternalPaths(external) => {
1897 paths.extend(external.paths().iter().cloned());
1898 }
1899 ClipboardEntry::String(text) => {
1900 if metadata.is_none() {
1901 metadata = text.metadata_json::<CopyMetadata>();
1902 }
1903 }
1904 }
1905 }
1906 }
1907
1908 let default_image_name: SharedString = "Image".into();
1909 for path in paths {
1910 let Some((image, _)) = load_external_image_from_path(&path, &default_image_name) else {
1911 continue;
1912 };
1913 images.push(image);
1914 }
1915
1916 // Respect entry priority order — if the first entry is text, the source
1917 // application considers text the primary content. Discard collected images
1918 // so the text-paste branch runs instead.
1919 if clipboard_item
1920 .as_ref()
1921 .and_then(|item| item.entries().first())
1922 .is_some_and(|entry| matches!(entry, ClipboardEntry::String(_)))
1923 {
1924 images.clear();
1925 }
1926
1927 if images.is_empty() {
1928 self.editor.update(cx, |editor, cx| {
1929 let paste_position = editor
1930 .selections
1931 .newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
1932 .head();
1933 editor.paste(action, window, cx);
1934
1935 if let Some(metadata) = metadata {
1936 let buffer = editor.buffer().read(cx).snapshot(cx);
1937
1938 let mut buffer_rows_to_fold = BTreeSet::new();
1939 let weak_editor = cx.entity().downgrade();
1940 editor.insert_creases(
1941 metadata.creases.into_iter().map(|metadata| {
1942 let start = buffer.anchor_after(
1943 paste_position + metadata.range_relative_to_selection.start,
1944 );
1945 let end = buffer.anchor_before(
1946 paste_position + metadata.range_relative_to_selection.end,
1947 );
1948
1949 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
1950 buffer_rows_to_fold.insert(buffer_row);
1951 Crease::inline(
1952 start..end,
1953 FoldPlaceholder {
1954 render: render_fold_icon_button(
1955 weak_editor.clone(),
1956 metadata.crease.icon_path.clone(),
1957 metadata.crease.label.clone(),
1958 ),
1959 ..Default::default()
1960 },
1961 render_slash_command_output_toggle,
1962 |_, _, _, _| Empty.into_any(),
1963 )
1964 .with_metadata(metadata.crease)
1965 }),
1966 cx,
1967 );
1968 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
1969 editor.fold_at(buffer_row, window, cx);
1970 }
1971 }
1972 });
1973 } else {
1974 let mut image_positions = Vec::new();
1975 self.editor.update(cx, |editor, cx| {
1976 editor.transact(window, cx, |editor, _window, cx| {
1977 let edits = editor
1978 .selections
1979 .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
1980 .into_iter()
1981 .map(|selection| (selection.start..selection.end, "\n"));
1982 editor.edit(edits, cx);
1983
1984 let snapshot = editor.buffer().read(cx).snapshot(cx);
1985 for selection in editor
1986 .selections
1987 .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
1988 {
1989 image_positions.push(snapshot.anchor_before(selection.end));
1990 }
1991 });
1992 });
1993
1994 self.text_thread.update(cx, |text_thread, cx| {
1995 for image in images {
1996 let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err()
1997 else {
1998 continue;
1999 };
2000 let image_id = image.id();
2001 let image_task = LanguageModelImage::from_image(Arc::new(image), cx).shared();
2002
2003 for image_position in image_positions.iter() {
2004 text_thread.insert_content(
2005 Content::Image {
2006 anchor: image_position.text_anchor,
2007 image_id,
2008 image: image_task.clone(),
2009 render_image: render_image.clone(),
2010 },
2011 cx,
2012 );
2013 }
2014 }
2015 });
2016 }
2017 }
2018
2019 fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
2020 self.editor.update(cx, |editor, cx| {
2021 editor.paste(&editor::actions::Paste, window, cx);
2022 });
2023 }
2024
2025 fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
2026 self.editor.update(cx, |editor, cx| {
2027 let buffer = editor.buffer().read(cx).snapshot(cx);
2028 let excerpt_id = buffer.as_singleton().unwrap().0;
2029 let old_blocks = std::mem::take(&mut self.image_blocks);
2030 let new_blocks = self
2031 .text_thread
2032 .read(cx)
2033 .contents(cx)
2034 .map(
2035 |Content::Image {
2036 anchor,
2037 render_image,
2038 ..
2039 }| (anchor, render_image),
2040 )
2041 .filter_map(|(anchor, render_image)| {
2042 const MAX_HEIGHT_IN_LINES: u32 = 8;
2043 let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
2044 let image = render_image;
2045 anchor.is_valid(&buffer).then(|| BlockProperties {
2046 placement: BlockPlacement::Above(anchor),
2047 height: Some(MAX_HEIGHT_IN_LINES),
2048 style: BlockStyle::Sticky,
2049 render: Arc::new(move |cx| {
2050 let image_size = size_for_image(
2051 &image,
2052 size(
2053 cx.max_width - cx.margins.gutter.full_width(),
2054 MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
2055 ),
2056 );
2057 h_flex()
2058 .pl(cx.margins.gutter.full_width())
2059 .child(
2060 img(image.clone())
2061 .object_fit(gpui::ObjectFit::ScaleDown)
2062 .w(image_size.width)
2063 .h(image_size.height),
2064 )
2065 .into_any_element()
2066 }),
2067 priority: 0,
2068 })
2069 })
2070 .collect::<Vec<_>>();
2071
2072 editor.remove_blocks(old_blocks, None, cx);
2073 let ids = editor.insert_blocks(new_blocks, None, cx);
2074 self.image_blocks = HashSet::from_iter(ids);
2075 });
2076 }
2077
2078 fn split(&mut self, _: &Split, _window: &mut Window, cx: &mut Context<Self>) {
2079 self.text_thread.update(cx, |text_thread, cx| {
2080 let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
2081 for selection in selections.as_ref() {
2082 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2083 let range = selection
2084 .map(|endpoint| endpoint.to_offset(&buffer))
2085 .range();
2086 text_thread.split_message(range.start.0..range.end.0, cx);
2087 }
2088 });
2089 }
2090
2091 fn save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
2092 self.text_thread.update(cx, |text_thread, cx| {
2093 text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
2094 });
2095 }
2096
2097 pub fn title(&self, cx: &App) -> SharedString {
2098 self.text_thread.read(cx).summary().or_default()
2099 }
2100
2101 pub fn regenerate_summary(&mut self, cx: &mut Context<Self>) {
2102 self.text_thread
2103 .update(cx, |text_thread, cx| text_thread.summarize(true, cx));
2104 }
2105
2106 fn render_remaining_tokens(&self, cx: &App) -> Option<impl IntoElement + use<>> {
2107 let (token_count_color, token_count, max_token_count, tooltip) =
2108 match token_state(&self.text_thread, cx)? {
2109 TokenState::NoTokensLeft {
2110 max_token_count,
2111 token_count,
2112 } => (
2113 Color::Error,
2114 token_count,
2115 max_token_count,
2116 Some("Token Limit Reached"),
2117 ),
2118 TokenState::HasMoreTokens {
2119 max_token_count,
2120 token_count,
2121 over_warn_threshold,
2122 } => {
2123 let (color, tooltip) = if over_warn_threshold {
2124 (Color::Warning, Some("Token Limit is Close to Exhaustion"))
2125 } else {
2126 (Color::Muted, None)
2127 };
2128 (color, token_count, max_token_count, tooltip)
2129 }
2130 };
2131
2132 Some(
2133 h_flex()
2134 .id("token-count")
2135 .gap_0p5()
2136 .child(
2137 Label::new(humanize_token_count(token_count))
2138 .size(LabelSize::Small)
2139 .color(token_count_color),
2140 )
2141 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2142 .child(
2143 Label::new(humanize_token_count(max_token_count))
2144 .size(LabelSize::Small)
2145 .color(Color::Muted),
2146 )
2147 .when_some(tooltip, |element, tooltip| {
2148 element.tooltip(Tooltip::text(tooltip))
2149 }),
2150 )
2151 }
2152
2153 fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2154 let focus_handle = self.focus_handle(cx);
2155
2156 let (style, tooltip) = match token_state(&self.text_thread, cx) {
2157 Some(TokenState::NoTokensLeft { .. }) => (
2158 ButtonStyle::Tinted(TintColor::Error),
2159 Some(Tooltip::text("Token limit reached")(window, cx)),
2160 ),
2161 Some(TokenState::HasMoreTokens {
2162 over_warn_threshold,
2163 ..
2164 }) => {
2165 let (style, tooltip) = if over_warn_threshold {
2166 (
2167 ButtonStyle::Tinted(TintColor::Warning),
2168 Some(Tooltip::text("Token limit is close to exhaustion")(
2169 window, cx,
2170 )),
2171 )
2172 } else {
2173 (ButtonStyle::Filled, None)
2174 };
2175 (style, tooltip)
2176 }
2177 None => (ButtonStyle::Filled, None),
2178 };
2179
2180 Button::new("send_button", "Send")
2181 .label_size(LabelSize::Small)
2182 .disabled(self.sending_disabled(cx))
2183 .style(style)
2184 .when_some(tooltip, |button, tooltip| {
2185 button.tooltip(move |_, _| tooltip.clone())
2186 })
2187 .layer(ElevationIndex::ModalSurface)
2188 .key_binding(
2189 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
2190 .map(|kb| kb.size(rems_from_px(12.))),
2191 )
2192 .on_click(move |_event, window, cx| {
2193 focus_handle.dispatch_action(&Assist, window, cx);
2194 })
2195 }
2196
2197 /// Whether or not we should allow messages to be sent.
2198 /// Will return false if the selected provided has a configuration error or
2199 /// if the user has not accepted the terms of service for this provider.
2200 fn sending_disabled(&self, cx: &mut Context<'_, TextThreadEditor>) -> bool {
2201 let model_registry = LanguageModelRegistry::read_global(cx);
2202 let Some(configuration_error) =
2203 model_registry.configuration_error(model_registry.default_model(), cx)
2204 else {
2205 return false;
2206 };
2207
2208 match configuration_error {
2209 ConfigurationError::NoProvider
2210 | ConfigurationError::ModelNotFound
2211 | ConfigurationError::ProviderNotAuthenticated(_) => true,
2212 }
2213 }
2214
2215 fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
2216 slash_command_picker::SlashCommandSelector::new(
2217 self.slash_commands.clone(),
2218 cx.entity().downgrade(),
2219 IconButton::new("trigger", IconName::Plus)
2220 .icon_size(IconSize::Small)
2221 .icon_color(Color::Muted)
2222 .selected_icon_color(Color::Accent)
2223 .selected_style(ButtonStyle::Filled),
2224 move |_window, cx| {
2225 Tooltip::with_meta("Add Context", None, "Type / to insert via keyboard", cx)
2226 },
2227 )
2228 }
2229
2230 fn render_language_model_selector(
2231 &self,
2232 window: &mut Window,
2233 cx: &mut Context<Self>,
2234 ) -> impl IntoElement {
2235 let active_model = LanguageModelRegistry::read_global(cx)
2236 .default_model()
2237 .map(|default| default.model);
2238 let model_name = match active_model {
2239 Some(model) => model.name().0,
2240 None => SharedString::from("Select Model"),
2241 };
2242
2243 let active_provider = LanguageModelRegistry::read_global(cx)
2244 .default_model()
2245 .map(|default| default.provider);
2246
2247 let provider_icon = active_provider
2248 .as_ref()
2249 .map(|p| p.icon())
2250 .unwrap_or(IconOrSvg::Icon(IconName::ZedAgent));
2251
2252 let (color, icon) = if self.language_model_selector_menu_handle.is_deployed() {
2253 (Color::Accent, IconName::ChevronUp)
2254 } else {
2255 (Color::Muted, IconName::ChevronDown)
2256 };
2257
2258 let provider_icon_element = match provider_icon {
2259 IconOrSvg::Svg(path) => Icon::from_external_svg(path),
2260 IconOrSvg::Icon(name) => Icon::new(name),
2261 }
2262 .color(color)
2263 .size(IconSize::XSmall);
2264
2265 let show_cycle_row = self
2266 .language_model_selector
2267 .read(cx)
2268 .delegate
2269 .favorites_count()
2270 > 1;
2271
2272 let tooltip = Tooltip::element({
2273 move |_, _cx| {
2274 ModelSelectorTooltip::new()
2275 .show_cycle_row(show_cycle_row)
2276 .into_any_element()
2277 }
2278 });
2279
2280 PickerPopoverMenu::new(
2281 self.language_model_selector.clone(),
2282 Button::new("active-model", model_name)
2283 .color(color)
2284 .label_size(LabelSize::Small)
2285 .start_icon(provider_icon_element)
2286 .end_icon(Icon::new(icon).color(color).size(IconSize::XSmall)),
2287 tooltip,
2288 gpui::Corner::BottomRight,
2289 cx,
2290 )
2291 .with_handle(self.language_model_selector_menu_handle.clone())
2292 .render(window, cx)
2293 }
2294
2295 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2296 let last_error = self.last_error.as_ref()?;
2297
2298 Some(
2299 div()
2300 .absolute()
2301 .right_3()
2302 .bottom_12()
2303 .max_w_96()
2304 .py_2()
2305 .px_3()
2306 .elevation_2(cx)
2307 .occlude()
2308 .child(match last_error {
2309 AssistError::PaymentRequired => self.render_payment_required_error(cx),
2310 AssistError::Message(error_message) => {
2311 self.render_assist_error(error_message, cx)
2312 }
2313 })
2314 .into_any(),
2315 )
2316 }
2317
2318 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2319 const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
2320
2321 v_flex()
2322 .gap_0p5()
2323 .child(
2324 h_flex()
2325 .gap_1p5()
2326 .items_center()
2327 .child(Icon::new(IconName::XCircle).color(Color::Error))
2328 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2329 )
2330 .child(
2331 div()
2332 .id("error-message")
2333 .max_h_24()
2334 .overflow_y_scroll()
2335 .child(Label::new(ERROR_MESSAGE)),
2336 )
2337 .child(
2338 h_flex()
2339 .justify_end()
2340 .mt_1()
2341 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
2342 |this, _, _window, cx| {
2343 this.last_error = None;
2344 cx.open_url(&zed_urls::account_url(cx));
2345 cx.notify();
2346 },
2347 )))
2348 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2349 |this, _, _window, cx| {
2350 this.last_error = None;
2351 cx.notify();
2352 },
2353 ))),
2354 )
2355 .into_any()
2356 }
2357
2358 fn render_assist_error(
2359 &self,
2360 error_message: &SharedString,
2361 cx: &mut Context<Self>,
2362 ) -> AnyElement {
2363 v_flex()
2364 .gap_0p5()
2365 .child(
2366 h_flex()
2367 .gap_1p5()
2368 .items_center()
2369 .child(Icon::new(IconName::XCircle).color(Color::Error))
2370 .child(
2371 Label::new("Error interacting with language model")
2372 .weight(FontWeight::MEDIUM),
2373 ),
2374 )
2375 .child(
2376 div()
2377 .id("error-message")
2378 .max_h_32()
2379 .overflow_y_scroll()
2380 .child(Label::new(error_message.clone())),
2381 )
2382 .child(
2383 h_flex()
2384 .justify_end()
2385 .mt_1()
2386 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2387 |this, _, _window, cx| {
2388 this.last_error = None;
2389 cx.notify();
2390 },
2391 ))),
2392 )
2393 .into_any()
2394 }
2395}
2396
2397/// Returns the contents of the *outermost* fenced code block that contains the given offset.
2398fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> {
2399 const CODE_BLOCK_NODE: &str = "fenced_code_block";
2400 const CODE_BLOCK_CONTENT: &str = "code_fence_content";
2401
2402 let layer = snapshot.syntax_layers().next()?;
2403
2404 let root_node = layer.node();
2405 let mut cursor = root_node.walk();
2406
2407 // Go to the first child for the given offset
2408 while cursor.goto_first_child_for_byte(offset).is_some() {
2409 // If we're at the end of the node, go to the next one.
2410 // Example: if you have a fenced-code-block, and you're on the start of the line
2411 // right after the closing ```, you want to skip the fenced-code-block and
2412 // go to the next sibling.
2413 if cursor.node().end_byte() == offset {
2414 cursor.goto_next_sibling();
2415 }
2416
2417 if cursor.node().start_byte() > offset {
2418 break;
2419 }
2420
2421 // We found the fenced code block.
2422 if cursor.node().kind() == CODE_BLOCK_NODE {
2423 // Now we need to find the child node that contains the code.
2424 cursor.goto_first_child();
2425 loop {
2426 if cursor.node().kind() == CODE_BLOCK_CONTENT {
2427 return Some(cursor.node().byte_range());
2428 }
2429 if !cursor.goto_next_sibling() {
2430 break;
2431 }
2432 }
2433 }
2434 }
2435
2436 None
2437}
2438
2439fn render_thought_process_fold_icon_button(
2440 editor: WeakEntity<Editor>,
2441 status: ThoughtProcessStatus,
2442) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
2443 Arc::new(move |fold_id, fold_range, _cx| {
2444 let editor = editor.clone();
2445
2446 let button = ButtonLike::new(fold_id).layer(ElevationIndex::ElevatedSurface);
2447 let button = match status {
2448 ThoughtProcessStatus::Pending => button
2449 .child(
2450 Icon::new(IconName::ToolThink)
2451 .size(IconSize::Small)
2452 .color(Color::Muted),
2453 )
2454 .child(
2455 Label::new("Thinking…").color(Color::Muted).with_animation(
2456 "pulsating-label",
2457 Animation::new(Duration::from_secs(2))
2458 .repeat()
2459 .with_easing(pulsating_between(0.4, 0.8)),
2460 |label, delta| label.alpha(delta),
2461 ),
2462 ),
2463 ThoughtProcessStatus::Completed => button
2464 .style(ButtonStyle::Filled)
2465 .child(Icon::new(IconName::ToolThink).size(IconSize::Small))
2466 .child(Label::new("Thought Process").single_line()),
2467 };
2468
2469 button
2470 .on_click(move |_, window, cx| {
2471 editor
2472 .update(cx, |editor, cx| {
2473 let buffer_start = fold_range
2474 .start
2475 .to_point(&editor.buffer().read(cx).read(cx));
2476 let buffer_row = MultiBufferRow(buffer_start.row);
2477 editor.unfold_at(buffer_row, window, cx);
2478 })
2479 .ok();
2480 })
2481 .into_any_element()
2482 })
2483}
2484
2485fn render_fold_icon_button(
2486 editor: WeakEntity<Editor>,
2487 icon_path: SharedString,
2488 label: SharedString,
2489) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
2490 Arc::new(move |fold_id, fold_range, _cx| {
2491 let editor = editor.clone();
2492 ButtonLike::new(fold_id)
2493 .style(ButtonStyle::Filled)
2494 .layer(ElevationIndex::ElevatedSurface)
2495 .child(Icon::from_path(icon_path.clone()))
2496 .child(Label::new(label.clone()).single_line())
2497 .on_click(move |_, window, cx| {
2498 editor
2499 .update(cx, |editor, cx| {
2500 let buffer_start = fold_range
2501 .start
2502 .to_point(&editor.buffer().read(cx).read(cx));
2503 let buffer_row = MultiBufferRow(buffer_start.row);
2504 editor.unfold_at(buffer_row, window, cx);
2505 })
2506 .ok();
2507 })
2508 .into_any_element()
2509 })
2510}
2511
2512type ToggleFold = Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>;
2513
2514fn render_slash_command_output_toggle(
2515 row: MultiBufferRow,
2516 is_folded: bool,
2517 fold: ToggleFold,
2518 _window: &mut Window,
2519 _cx: &mut App,
2520) -> AnyElement {
2521 Disclosure::new(
2522 ("slash-command-output-fold-indicator", row.0 as u64),
2523 !is_folded,
2524 )
2525 .toggle_state(is_folded)
2526 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2527 .into_any_element()
2528}
2529
2530pub fn fold_toggle(
2531 name: &'static str,
2532) -> impl Fn(
2533 MultiBufferRow,
2534 bool,
2535 Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
2536 &mut Window,
2537 &mut App,
2538) -> AnyElement {
2539 move |row, is_folded, fold, _window, _cx| {
2540 Disclosure::new((name, row.0 as u64), !is_folded)
2541 .toggle_state(is_folded)
2542 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2543 .into_any_element()
2544 }
2545}
2546
2547fn quote_selection_fold_placeholder(title: String, editor: WeakEntity<Editor>) -> FoldPlaceholder {
2548 FoldPlaceholder {
2549 render: Arc::new({
2550 move |fold_id, fold_range, _cx| {
2551 let editor = editor.clone();
2552 ButtonLike::new(fold_id)
2553 .style(ButtonStyle::Filled)
2554 .layer(ElevationIndex::ElevatedSurface)
2555 .child(Icon::new(IconName::TextSnippet))
2556 .child(Label::new(title.clone()).single_line())
2557 .on_click(move |_, window, cx| {
2558 editor
2559 .update(cx, |editor, cx| {
2560 let buffer_start = fold_range
2561 .start
2562 .to_point(&editor.buffer().read(cx).read(cx));
2563 let buffer_row = MultiBufferRow(buffer_start.row);
2564 editor.unfold_at(buffer_row, window, cx);
2565 })
2566 .ok();
2567 })
2568 .into_any_element()
2569 }
2570 }),
2571 merge_adjacent: false,
2572 ..Default::default()
2573 }
2574}
2575
2576fn render_quote_selection_output_toggle(
2577 row: MultiBufferRow,
2578 is_folded: bool,
2579 fold: ToggleFold,
2580 _window: &mut Window,
2581 _cx: &mut App,
2582) -> AnyElement {
2583 Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
2584 .toggle_state(is_folded)
2585 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2586 .into_any_element()
2587}
2588
2589fn render_pending_slash_command_gutter_decoration(
2590 row: MultiBufferRow,
2591 status: &PendingSlashCommandStatus,
2592 confirm_command: Arc<dyn Fn(&mut Window, &mut App)>,
2593) -> AnyElement {
2594 let mut icon = IconButton::new(
2595 ("slash-command-gutter-decoration", row.0),
2596 ui::IconName::TriangleRight,
2597 )
2598 .on_click(move |_e, window, cx| confirm_command(window, cx))
2599 .icon_size(ui::IconSize::Small)
2600 .size(ui::ButtonSize::None);
2601
2602 match status {
2603 PendingSlashCommandStatus::Idle => {
2604 icon = icon.icon_color(Color::Muted);
2605 }
2606 PendingSlashCommandStatus::Running { .. } => {
2607 icon = icon.toggle_state(true);
2608 }
2609 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
2610 }
2611
2612 icon.into_any_element()
2613}
2614
2615#[derive(Debug, Clone, Serialize, Deserialize)]
2616struct CopyMetadata {
2617 creases: Vec<SelectedCreaseMetadata>,
2618}
2619
2620#[derive(Debug, Clone, Serialize, Deserialize)]
2621struct SelectedCreaseMetadata {
2622 range_relative_to_selection: Range<usize>,
2623 crease: CreaseMetadata,
2624}
2625
2626impl EventEmitter<EditorEvent> for TextThreadEditor {}
2627impl EventEmitter<SearchEvent> for TextThreadEditor {}
2628
2629impl Render for TextThreadEditor {
2630 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2631 let language_model_selector = self.language_model_selector_menu_handle.clone();
2632
2633 v_flex()
2634 .key_context("ContextEditor")
2635 .capture_action(cx.listener(TextThreadEditor::cancel))
2636 .capture_action(cx.listener(TextThreadEditor::save))
2637 .capture_action(cx.listener(TextThreadEditor::copy))
2638 .capture_action(cx.listener(TextThreadEditor::cut))
2639 .capture_action(cx.listener(TextThreadEditor::paste))
2640 .on_action(cx.listener(TextThreadEditor::paste_raw))
2641 .capture_action(cx.listener(TextThreadEditor::cycle_message_role))
2642 .capture_action(cx.listener(TextThreadEditor::confirm_command))
2643 .on_action(cx.listener(TextThreadEditor::assist))
2644 .on_action(cx.listener(TextThreadEditor::split))
2645 .on_action(move |_: &ToggleModelSelector, window, cx| {
2646 language_model_selector.toggle(window, cx);
2647 })
2648 .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
2649 this.language_model_selector.update(cx, |selector, cx| {
2650 selector.delegate.cycle_favorite_models(window, cx);
2651 });
2652 }))
2653 .size_full()
2654 .child(
2655 div()
2656 .flex_grow()
2657 .bg(cx.theme().colors().editor_background)
2658 .child(self.editor.clone()),
2659 )
2660 .children(self.render_last_error(cx))
2661 .child(
2662 h_flex()
2663 .relative()
2664 .py_2()
2665 .pl_1p5()
2666 .pr_2()
2667 .w_full()
2668 .justify_between()
2669 .border_t_1()
2670 .border_color(cx.theme().colors().border_variant)
2671 .bg(cx.theme().colors().editor_background)
2672 .child(
2673 h_flex()
2674 .gap_0p5()
2675 .child(self.render_inject_context_menu(cx)),
2676 )
2677 .child(
2678 h_flex()
2679 .gap_2p5()
2680 .children(self.render_remaining_tokens(cx))
2681 .child(
2682 h_flex()
2683 .gap_1()
2684 .child(self.render_language_model_selector(window, cx))
2685 .child(self.render_send_button(window, cx)),
2686 ),
2687 ),
2688 )
2689 }
2690}
2691
2692impl Focusable for TextThreadEditor {
2693 fn focus_handle(&self, cx: &App) -> FocusHandle {
2694 self.editor.focus_handle(cx)
2695 }
2696}
2697
2698impl Item for TextThreadEditor {
2699 type Event = editor::EditorEvent;
2700
2701 fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
2702 util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into()
2703 }
2704
2705 fn to_item_events(event: &Self::Event, f: &mut dyn FnMut(item::ItemEvent)) {
2706 match event {
2707 EditorEvent::Edited { .. } => {
2708 f(item::ItemEvent::Edit);
2709 }
2710 EditorEvent::TitleChanged => {
2711 f(item::ItemEvent::UpdateTab);
2712 }
2713 _ => {}
2714 }
2715 }
2716
2717 fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
2718 Some(self.title(cx).to_string().into())
2719 }
2720
2721 fn as_searchable(
2722 &self,
2723 handle: &Entity<Self>,
2724 _: &App,
2725 ) -> Option<Box<dyn SearchableItemHandle>> {
2726 Some(Box::new(handle.clone()))
2727 }
2728
2729 fn set_nav_history(
2730 &mut self,
2731 nav_history: pane::ItemNavHistory,
2732 window: &mut Window,
2733 cx: &mut Context<Self>,
2734 ) {
2735 self.editor.update(cx, |editor, cx| {
2736 Item::set_nav_history(editor, nav_history, window, cx)
2737 })
2738 }
2739
2740 fn navigate(
2741 &mut self,
2742 data: Arc<dyn Any + Send>,
2743 window: &mut Window,
2744 cx: &mut Context<Self>,
2745 ) -> bool {
2746 self.editor
2747 .update(cx, |editor, cx| Item::navigate(editor, data, window, cx))
2748 }
2749
2750 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2751 self.editor
2752 .update(cx, |editor, cx| Item::deactivated(editor, window, cx))
2753 }
2754
2755 fn act_as_type<'a>(
2756 &'a self,
2757 type_id: TypeId,
2758 self_handle: &'a Entity<Self>,
2759 _: &'a App,
2760 ) -> Option<gpui::AnyEntity> {
2761 if type_id == TypeId::of::<Self>() {
2762 Some(self_handle.clone().into())
2763 } else if type_id == TypeId::of::<Editor>() {
2764 Some(self.editor.clone().into())
2765 } else {
2766 None
2767 }
2768 }
2769
2770 fn include_in_nav_history() -> bool {
2771 false
2772 }
2773}
2774
2775impl SearchableItem for TextThreadEditor {
2776 type Match = <Editor as SearchableItem>::Match;
2777
2778 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2779 self.editor.update(cx, |editor, cx| {
2780 editor.clear_matches(window, cx);
2781 });
2782 }
2783
2784 fn update_matches(
2785 &mut self,
2786 matches: &[Self::Match],
2787 active_match_index: Option<usize>,
2788 token: SearchToken,
2789 window: &mut Window,
2790 cx: &mut Context<Self>,
2791 ) {
2792 self.editor.update(cx, |editor, cx| {
2793 editor.update_matches(matches, active_match_index, token, window, cx)
2794 });
2795 }
2796
2797 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
2798 self.editor
2799 .update(cx, |editor, cx| editor.query_suggestion(window, cx))
2800 }
2801
2802 fn activate_match(
2803 &mut self,
2804 index: usize,
2805 matches: &[Self::Match],
2806 token: SearchToken,
2807 window: &mut Window,
2808 cx: &mut Context<Self>,
2809 ) {
2810 self.editor.update(cx, |editor, cx| {
2811 editor.activate_match(index, matches, token, window, cx);
2812 });
2813 }
2814
2815 fn select_matches(
2816 &mut self,
2817 matches: &[Self::Match],
2818 token: SearchToken,
2819 window: &mut Window,
2820 cx: &mut Context<Self>,
2821 ) {
2822 self.editor.update(cx, |editor, cx| {
2823 editor.select_matches(matches, token, window, cx)
2824 });
2825 }
2826
2827 fn replace(
2828 &mut self,
2829 identifier: &Self::Match,
2830 query: &project::search::SearchQuery,
2831 token: SearchToken,
2832 window: &mut Window,
2833 cx: &mut Context<Self>,
2834 ) {
2835 self.editor.update(cx, |editor, cx| {
2836 editor.replace(identifier, query, token, window, cx)
2837 });
2838 }
2839
2840 fn find_matches(
2841 &mut self,
2842 query: Arc<project::search::SearchQuery>,
2843 window: &mut Window,
2844 cx: &mut Context<Self>,
2845 ) -> Task<Vec<Self::Match>> {
2846 self.editor
2847 .update(cx, |editor, cx| editor.find_matches(query, window, cx))
2848 }
2849
2850 fn active_match_index(
2851 &mut self,
2852 direction: Direction,
2853 matches: &[Self::Match],
2854 token: SearchToken,
2855 window: &mut Window,
2856 cx: &mut Context<Self>,
2857 ) -> Option<usize> {
2858 self.editor.update(cx, |editor, cx| {
2859 editor.active_match_index(direction, matches, token, window, cx)
2860 })
2861 }
2862}
2863
2864impl FollowableItem for TextThreadEditor {
2865 fn remote_id(&self) -> Option<workspace::ViewId> {
2866 self.remote_id
2867 }
2868
2869 fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
2870 let context_id = self.text_thread.read(cx).id().to_proto();
2871 let editor_proto = self
2872 .editor
2873 .update(cx, |editor, cx| editor.to_state_proto(window, cx));
2874 Some(proto::view::Variant::ContextEditor(
2875 proto::view::ContextEditor {
2876 context_id,
2877 editor: if let Some(proto::view::Variant::Editor(proto)) = editor_proto {
2878 Some(proto)
2879 } else {
2880 None
2881 },
2882 },
2883 ))
2884 }
2885
2886 fn from_state_proto(
2887 workspace: Entity<Workspace>,
2888 id: workspace::ViewId,
2889 state: &mut Option<proto::view::Variant>,
2890 window: &mut Window,
2891 cx: &mut App,
2892 ) -> Option<Task<Result<Entity<Self>>>> {
2893 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
2894 return None;
2895 };
2896 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
2897 unreachable!()
2898 };
2899
2900 let text_thread_id = TextThreadId::from_proto(state.context_id);
2901 let editor_state = state.editor?;
2902
2903 let project = workspace.read(cx).project().clone();
2904 let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
2905
2906 let text_thread_editor_task = workspace.update(cx, |workspace, cx| {
2907 agent_panel_delegate.open_remote_text_thread(workspace, text_thread_id, window, cx)
2908 });
2909
2910 Some(window.spawn(cx, async move |cx| {
2911 let text_thread_editor = text_thread_editor_task.await?;
2912 text_thread_editor
2913 .update_in(cx, |text_thread_editor, window, cx| {
2914 text_thread_editor.remote_id = Some(id);
2915 text_thread_editor.editor.update(cx, |editor, cx| {
2916 editor.apply_update_proto(
2917 &project,
2918 proto::update_view::Variant::Editor(proto::update_view::Editor {
2919 selections: editor_state.selections,
2920 pending_selection: editor_state.pending_selection,
2921 scroll_top_anchor: editor_state.scroll_top_anchor,
2922 scroll_x: editor_state.scroll_y,
2923 scroll_y: editor_state.scroll_y,
2924 ..Default::default()
2925 }),
2926 window,
2927 cx,
2928 )
2929 })
2930 })?
2931 .await?;
2932 Ok(text_thread_editor)
2933 }))
2934 }
2935
2936 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
2937 Editor::to_follow_event(event)
2938 }
2939
2940 fn add_event_to_update_proto(
2941 &self,
2942 event: &Self::Event,
2943 update: &mut Option<proto::update_view::Variant>,
2944 window: &mut Window,
2945 cx: &mut App,
2946 ) -> bool {
2947 self.editor.update(cx, |editor, cx| {
2948 editor.add_event_to_update_proto(event, update, window, cx)
2949 })
2950 }
2951
2952 fn apply_update_proto(
2953 &mut self,
2954 project: &Entity<Project>,
2955 message: proto::update_view::Variant,
2956 window: &mut Window,
2957 cx: &mut Context<Self>,
2958 ) -> Task<Result<()>> {
2959 self.editor.update(cx, |editor, cx| {
2960 editor.apply_update_proto(project, message, window, cx)
2961 })
2962 }
2963
2964 fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {
2965 true
2966 }
2967
2968 fn set_leader_id(
2969 &mut self,
2970 leader_id: Option<CollaboratorId>,
2971 window: &mut Window,
2972 cx: &mut Context<Self>,
2973 ) {
2974 self.editor
2975 .update(cx, |editor, cx| editor.set_leader_id(leader_id, window, cx))
2976 }
2977
2978 fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option<item::Dedup> {
2979 if existing.text_thread.read(cx).id() == self.text_thread.read(cx).id() {
2980 Some(item::Dedup::KeepExisting)
2981 } else {
2982 None
2983 }
2984 }
2985}
2986
2987enum PendingSlashCommand {}
2988
2989fn invoked_slash_command_fold_placeholder(
2990 command_id: InvokedSlashCommandId,
2991 text_thread: WeakEntity<TextThread>,
2992) -> FoldPlaceholder {
2993 FoldPlaceholder {
2994 collapsed_text: None,
2995 constrain_width: false,
2996 merge_adjacent: false,
2997 render: Arc::new(move |fold_id, _, cx| {
2998 let Some(text_thread) = text_thread.upgrade() else {
2999 return Empty.into_any();
3000 };
3001
3002 let Some(command) = text_thread.read(cx).invoked_slash_command(&command_id) else {
3003 return Empty.into_any();
3004 };
3005
3006 h_flex()
3007 .id(fold_id)
3008 .px_1()
3009 .ml_6()
3010 .gap_2()
3011 .bg(cx.theme().colors().surface_background)
3012 .rounded_sm()
3013 .child(Label::new(format!("/{}", command.name)))
3014 .map(|parent| match &command.status {
3015 InvokedSlashCommandStatus::Running(_) => {
3016 parent.child(Icon::new(IconName::ArrowCircle).with_rotate_animation(4))
3017 }
3018 InvokedSlashCommandStatus::Error(message) => parent.child(
3019 Label::new(format!("error: {message}"))
3020 .single_line()
3021 .color(Color::Error),
3022 ),
3023 InvokedSlashCommandStatus::Finished => parent,
3024 })
3025 .into_any_element()
3026 }),
3027 type_tag: Some(TypeId::of::<PendingSlashCommand>()),
3028 }
3029}
3030
3031enum TokenState {
3032 NoTokensLeft {
3033 max_token_count: u64,
3034 token_count: u64,
3035 },
3036 HasMoreTokens {
3037 max_token_count: u64,
3038 token_count: u64,
3039 over_warn_threshold: bool,
3040 },
3041}
3042
3043fn token_state(text_thread: &Entity<TextThread>, cx: &App) -> Option<TokenState> {
3044 const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
3045
3046 let model = LanguageModelRegistry::read_global(cx)
3047 .default_model()?
3048 .model;
3049 let token_count = text_thread.read(cx).token_count()?;
3050 let max_token_count = model.max_token_count();
3051 let token_state = if max_token_count.saturating_sub(token_count) == 0 {
3052 TokenState::NoTokensLeft {
3053 max_token_count,
3054 token_count,
3055 }
3056 } else {
3057 let over_warn_threshold =
3058 token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
3059 TokenState::HasMoreTokens {
3060 max_token_count,
3061 token_count,
3062 over_warn_threshold,
3063 }
3064 };
3065 Some(token_state)
3066}
3067
3068fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
3069 let image_size = data
3070 .size(0)
3071 .map(|dimension| Pixels::from(u32::from(dimension)));
3072 let image_ratio = image_size.width / image_size.height;
3073 let bounds_ratio = max_size.width / max_size.height;
3074
3075 if image_size.width > max_size.width || image_size.height > max_size.height {
3076 if bounds_ratio > image_ratio {
3077 size(
3078 image_size.width * (max_size.height / image_size.height),
3079 max_size.height,
3080 )
3081 } else {
3082 size(
3083 max_size.width,
3084 image_size.height * (max_size.width / image_size.width),
3085 )
3086 }
3087 } else {
3088 size(image_size.width, image_size.height)
3089 }
3090}
3091
3092pub fn humanize_token_count(count: u64) -> String {
3093 match count {
3094 0..=999 => count.to_string(),
3095 1000..=9999 => {
3096 let thousands = count / 1000;
3097 let hundreds = (count % 1000 + 50) / 100;
3098 if hundreds == 0 {
3099 format!("{}k", thousands)
3100 } else if hundreds == 10 {
3101 format!("{}k", thousands + 1)
3102 } else {
3103 format!("{}.{}k", thousands, hundreds)
3104 }
3105 }
3106 1_000_000..=9_999_999 => {
3107 let millions = count / 1_000_000;
3108 let hundred_thousands = (count % 1_000_000 + 50_000) / 100_000;
3109 if hundred_thousands == 0 {
3110 format!("{}M", millions)
3111 } else if hundred_thousands == 10 {
3112 format!("{}M", millions + 1)
3113 } else {
3114 format!("{}.{}M", millions, hundred_thousands)
3115 }
3116 }
3117 10_000_000.. => format!("{}M", (count + 500_000) / 1_000_000),
3118 _ => format!("{}k", (count + 500) / 1000),
3119 }
3120}
3121
3122pub fn make_lsp_adapter_delegate(
3123 project: &Entity<Project>,
3124 cx: &mut App,
3125) -> Result<Option<Arc<dyn LspAdapterDelegate>>> {
3126 project.update(cx, |project, cx| {
3127 // TODO: Find the right worktree.
3128 let Some(worktree) = project.worktrees(cx).next() else {
3129 return Ok(None::<Arc<dyn LspAdapterDelegate>>);
3130 };
3131 let http_client = project.client().http_client();
3132 project.lsp_store().update(cx, |_, cx| {
3133 Ok(Some(LocalLspAdapterDelegate::new(
3134 project.languages().clone(),
3135 project.environment(),
3136 cx.weak_entity(),
3137 &worktree,
3138 http_client,
3139 project.fs().clone(),
3140 cx,
3141 ) as Arc<dyn LspAdapterDelegate>))
3142 })
3143 })
3144}
3145
3146#[cfg(test)]
3147mod tests {
3148 use super::*;
3149 use editor::{MultiBufferOffset, SelectionEffects};
3150 use fs::FakeFs;
3151 use gpui::{App, TestAppContext, VisualTestContext};
3152 use indoc::indoc;
3153 use language::{Buffer, LanguageRegistry};
3154 use pretty_assertions::assert_eq;
3155 use prompt_store::PromptBuilder;
3156 use text::OffsetRangeExt;
3157 use unindent::Unindent;
3158 use util::path;
3159 use workspace::MultiWorkspace;
3160
3161 #[gpui::test]
3162 async fn test_copy_paste_whole_message(cx: &mut TestAppContext) {
3163 let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text(vec![
3164 (Role::User, "What is the Zed editor?"),
3165 (
3166 Role::Assistant,
3167 "Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.",
3168 ),
3169 (Role::User, ""),
3170 ],cx).await;
3171
3172 // Select & Copy whole user message
3173 assert_copy_paste_text_thread_editor(
3174 &text_thread_editor,
3175 message_range(&context, 0, &mut cx),
3176 indoc! {"
3177 What is the Zed editor?
3178 Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
3179 What is the Zed editor?
3180 "},
3181 &mut cx,
3182 );
3183
3184 // Select & Copy whole assistant message
3185 assert_copy_paste_text_thread_editor(
3186 &text_thread_editor,
3187 message_range(&context, 1, &mut cx),
3188 indoc! {"
3189 What is the Zed editor?
3190 Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
3191 What is the Zed editor?
3192 Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
3193 "},
3194 &mut cx,
3195 );
3196 }
3197
3198 #[gpui::test]
3199 async fn test_copy_paste_no_selection(cx: &mut TestAppContext) {
3200 let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text(
3201 vec![
3202 (Role::User, "user1"),
3203 (Role::Assistant, "assistant1"),
3204 (Role::Assistant, "assistant2"),
3205 (Role::User, ""),
3206 ],
3207 cx,
3208 )
3209 .await;
3210
3211 // Copy and paste first assistant message
3212 let message_2_range = message_range(&context, 1, &mut cx);
3213 assert_copy_paste_text_thread_editor(
3214 &text_thread_editor,
3215 message_2_range.start..message_2_range.start,
3216 indoc! {"
3217 user1
3218 assistant1
3219 assistant2
3220 assistant1
3221 "},
3222 &mut cx,
3223 );
3224
3225 // Copy and cut second assistant message
3226 let message_3_range = message_range(&context, 2, &mut cx);
3227 assert_copy_paste_text_thread_editor(
3228 &text_thread_editor,
3229 message_3_range.start..message_3_range.start,
3230 indoc! {"
3231 user1
3232 assistant1
3233 assistant2
3234 assistant1
3235 assistant2
3236 "},
3237 &mut cx,
3238 );
3239 }
3240
3241 #[gpui::test]
3242 fn test_find_code_blocks(cx: &mut App) {
3243 let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
3244
3245 let buffer = cx.new(|cx| {
3246 let text = r#"
3247 line 0
3248 line 1
3249 ```rust
3250 fn main() {}
3251 ```
3252 line 5
3253 line 6
3254 line 7
3255 ```go
3256 func main() {}
3257 ```
3258 line 11
3259 ```
3260 this is plain text code block
3261 ```
3262
3263 ```go
3264 func another() {}
3265 ```
3266 line 19
3267 "#
3268 .unindent();
3269 let mut buffer = Buffer::local(text, cx);
3270 buffer.set_language(Some(markdown.clone()), cx);
3271 buffer
3272 });
3273 let snapshot = buffer.read(cx).snapshot();
3274
3275 let code_blocks = vec![
3276 Point::new(3, 0)..Point::new(4, 0),
3277 Point::new(9, 0)..Point::new(10, 0),
3278 Point::new(13, 0)..Point::new(14, 0),
3279 Point::new(17, 0)..Point::new(18, 0),
3280 ]
3281 .into_iter()
3282 .map(|range| snapshot.point_to_offset(range.start)..snapshot.point_to_offset(range.end))
3283 .collect::<Vec<_>>();
3284
3285 let expected_results = vec![
3286 (0, None),
3287 (1, None),
3288 (2, Some(code_blocks[0].clone())),
3289 (3, Some(code_blocks[0].clone())),
3290 (4, Some(code_blocks[0].clone())),
3291 (5, None),
3292 (6, None),
3293 (7, None),
3294 (8, Some(code_blocks[1].clone())),
3295 (9, Some(code_blocks[1].clone())),
3296 (10, Some(code_blocks[1].clone())),
3297 (11, None),
3298 (12, Some(code_blocks[2].clone())),
3299 (13, Some(code_blocks[2].clone())),
3300 (14, Some(code_blocks[2].clone())),
3301 (15, None),
3302 (16, Some(code_blocks[3].clone())),
3303 (17, Some(code_blocks[3].clone())),
3304 (18, Some(code_blocks[3].clone())),
3305 (19, None),
3306 ];
3307
3308 for (row, expected) in expected_results {
3309 let offset = snapshot.point_to_offset(Point::new(row, 0));
3310 let range = find_surrounding_code_block(&snapshot, offset);
3311 assert_eq!(range, expected, "unexpected result on row {:?}", row);
3312 }
3313 }
3314
3315 async fn setup_text_thread_editor_text(
3316 messages: Vec<(Role, &str)>,
3317 cx: &mut TestAppContext,
3318 ) -> (
3319 Entity<TextThread>,
3320 Entity<TextThreadEditor>,
3321 VisualTestContext,
3322 ) {
3323 cx.update(init_test);
3324
3325 let fs = FakeFs::new(cx.executor());
3326 let text_thread = create_text_thread_with_messages(messages, cx);
3327
3328 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
3329 let window_handle =
3330 cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3331 let workspace = window_handle
3332 .read_with(cx, |mw, _| mw.workspace().clone())
3333 .unwrap();
3334 let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
3335
3336 let weak_workspace = workspace.downgrade();
3337 let text_thread_editor = workspace.update_in(&mut cx, |_, window, cx| {
3338 cx.new(|cx| {
3339 TextThreadEditor::for_text_thread(
3340 text_thread.clone(),
3341 fs,
3342 weak_workspace,
3343 project,
3344 None,
3345 window,
3346 cx,
3347 )
3348 })
3349 });
3350
3351 (text_thread, text_thread_editor, cx)
3352 }
3353
3354 fn message_range(
3355 text_thread: &Entity<TextThread>,
3356 message_ix: usize,
3357 cx: &mut TestAppContext,
3358 ) -> Range<MultiBufferOffset> {
3359 let range = text_thread.update(cx, |text_thread, cx| {
3360 text_thread
3361 .messages(cx)
3362 .nth(message_ix)
3363 .unwrap()
3364 .anchor_range
3365 .to_offset(&text_thread.buffer().read(cx).snapshot())
3366 });
3367 MultiBufferOffset(range.start)..MultiBufferOffset(range.end)
3368 }
3369
3370 fn assert_copy_paste_text_thread_editor<T: editor::ToOffset>(
3371 text_thread_editor: &Entity<TextThreadEditor>,
3372 range: Range<T>,
3373 expected_text: &str,
3374 cx: &mut VisualTestContext,
3375 ) {
3376 text_thread_editor.update_in(cx, |text_thread_editor, window, cx| {
3377 text_thread_editor.editor.update(cx, |editor, cx| {
3378 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3379 s.select_ranges([range])
3380 });
3381 });
3382
3383 text_thread_editor.copy(&Default::default(), window, cx);
3384
3385 text_thread_editor.editor.update(cx, |editor, cx| {
3386 editor.move_to_end(&Default::default(), window, cx);
3387 });
3388
3389 text_thread_editor.paste(&Default::default(), window, cx);
3390
3391 text_thread_editor.editor.update(cx, |editor, cx| {
3392 assert_eq!(editor.text(cx), expected_text);
3393 });
3394 });
3395 }
3396
3397 fn create_text_thread_with_messages(
3398 mut messages: Vec<(Role, &str)>,
3399 cx: &mut TestAppContext,
3400 ) -> Entity<TextThread> {
3401 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
3402 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
3403 cx.new(|cx| {
3404 let mut text_thread = TextThread::local(
3405 registry,
3406 prompt_builder.clone(),
3407 Arc::new(SlashCommandWorkingSet::default()),
3408 cx,
3409 );
3410 let mut message_1 = text_thread.messages(cx).next().unwrap();
3411 let (role, text) = messages.remove(0);
3412
3413 loop {
3414 if role == message_1.role {
3415 text_thread.buffer().update(cx, |buffer, cx| {
3416 buffer.edit([(message_1.offset_range, text)], None, cx);
3417 });
3418 break;
3419 }
3420 let mut ids = HashSet::default();
3421 ids.insert(message_1.id);
3422 text_thread.cycle_message_roles(ids, cx);
3423 message_1 = text_thread.messages(cx).next().unwrap();
3424 }
3425
3426 let mut last_message_id = message_1.id;
3427 for (role, text) in messages {
3428 text_thread.insert_message_after(last_message_id, role, MessageStatus::Done, cx);
3429 let message = text_thread.messages(cx).last().unwrap();
3430 last_message_id = message.id;
3431 text_thread.buffer().update(cx, |buffer, cx| {
3432 buffer.edit([(message.offset_range, text)], None, cx);
3433 })
3434 }
3435
3436 text_thread
3437 })
3438 }
3439
3440 fn init_test(cx: &mut App) {
3441 let settings_store = SettingsStore::test(cx);
3442 prompt_store::init(cx);
3443 editor::init(cx);
3444 LanguageModelRegistry::test(cx);
3445 cx.set_global(settings_store);
3446
3447 theme_settings::init(theme::LoadThemes::JustBase, cx);
3448 }
3449
3450 #[gpui::test]
3451 async fn test_quote_terminal_text(cx: &mut TestAppContext) {
3452 let (_context, text_thread_editor, mut cx) =
3453 setup_text_thread_editor_text(vec![(Role::User, "")], cx).await;
3454
3455 let terminal_output = "$ ls -la\ntotal 0\ndrwxr-xr-x 2 user user 40 Jan 1 00:00 .";
3456
3457 text_thread_editor.update_in(&mut cx, |text_thread_editor, window, cx| {
3458 text_thread_editor.quote_terminal_text(terminal_output.to_string(), window, cx);
3459
3460 text_thread_editor.editor.update(cx, |editor, cx| {
3461 let text = editor.text(cx);
3462 // The text should contain the terminal output wrapped in a code block
3463 assert!(
3464 text.contains(&format!("```console\n{}\n```", terminal_output)),
3465 "Terminal text should be wrapped in code block. Got: {}",
3466 text
3467 );
3468 });
3469 });
3470 }
3471}