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