1use crate::ambient_context::{AmbientContext, RecentBuffer};
2use crate::{
3 assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel},
4 codegen::{self, Codegen, CodegenKind},
5 prompts::generate_content_prompt,
6 Assist, CompletionProvider, CycleMessageRole, InlineAssist, LanguageModel,
7 LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus,
8 QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, SavedMessage,
9 Split, ToggleFocus, ToggleHistory, ToggleIncludeConversation,
10};
11use anyhow::{anyhow, Result};
12use client::telemetry::Telemetry;
13use collections::{hash_map, HashMap, HashSet, VecDeque};
14use editor::{
15 actions::{MoveDown, MoveUp},
16 display_map::{
17 BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
18 },
19 scroll::{Autoscroll, AutoscrollStrategy},
20 Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBufferSnapshot, RowExt,
21 ToOffset as _, ToPoint,
22};
23use file_icons::FileIcons;
24use fs::Fs;
25use futures::StreamExt;
26use gpui::{
27 canvas, div, point, relative, rems, uniform_list, Action, AnyView, AppContext, AsyncAppContext,
28 AsyncWindowContext, AvailableSpace, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
29 FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model,
30 ModelContext, ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled,
31 Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
32 WeakModel, WeakView, WhiteSpace, WindowContext,
33};
34use language::{
35 language_settings::SoftWrap, Buffer, BufferSnapshot, DiagnosticEntry, LanguageRegistry, Point,
36 ToOffset as _,
37};
38use multi_buffer::MultiBufferRow;
39use parking_lot::Mutex;
40use project::Project;
41use search::{buffer_search::DivRegistrar, BufferSearchBar};
42use settings::Settings;
43use std::{
44 cmp,
45 fmt::Write,
46 iter,
47 ops::Range,
48 path::PathBuf,
49 sync::Arc,
50 time::{Duration, Instant},
51};
52use telemetry_events::AssistantKind;
53use theme::ThemeSettings;
54use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Tab, TabBar, Tooltip};
55use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
56use uuid::Uuid;
57use workspace::{
58 dock::{DockPosition, Panel, PanelEvent},
59 searchable::Direction,
60 Event as WorkspaceEvent, Save, Toast, ToggleZoom, Toolbar, Workspace,
61};
62use workspace::{notifications::NotificationId, NewFile};
63
64const MAX_RECENT_BUFFERS: usize = 3;
65
66pub fn init(cx: &mut AppContext) {
67 cx.observe_new_views(
68 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
69 workspace
70 .register_action(|workspace, _: &ToggleFocus, cx| {
71 let settings = AssistantSettings::get_global(cx);
72 if !settings.enabled {
73 return;
74 }
75
76 workspace.toggle_panel_focus::<AssistantPanel>(cx);
77 })
78 .register_action(AssistantPanel::inline_assist)
79 .register_action(AssistantPanel::cancel_last_inline_assist)
80 .register_action(ConversationEditor::quote_selection);
81 },
82 )
83 .detach();
84}
85
86pub struct AssistantPanel {
87 workspace: WeakView<Workspace>,
88 width: Option<Pixels>,
89 height: Option<Pixels>,
90 active_conversation_editor: Option<ActiveConversationEditor>,
91 show_saved_conversations: bool,
92 saved_conversations: Vec<SavedConversationMetadata>,
93 saved_conversations_scroll_handle: UniformListScrollHandle,
94 zoomed: bool,
95 focus_handle: FocusHandle,
96 toolbar: View<Toolbar>,
97 languages: Arc<LanguageRegistry>,
98 fs: Arc<dyn Fs>,
99 telemetry: Arc<Telemetry>,
100 _subscriptions: Vec<Subscription>,
101 next_inline_assist_id: usize,
102 pending_inline_assists: HashMap<usize, PendingInlineAssist>,
103 pending_inline_assist_ids_by_editor: HashMap<WeakView<Editor>, Vec<usize>>,
104 include_conversation_in_next_inline_assist: bool,
105 inline_prompt_history: VecDeque<String>,
106 _watch_saved_conversations: Task<Result<()>>,
107 model: LanguageModel,
108 authentication_prompt: Option<AnyView>,
109}
110
111struct ActiveConversationEditor {
112 editor: View<ConversationEditor>,
113 _subscriptions: Vec<Subscription>,
114}
115
116impl AssistantPanel {
117 const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20;
118
119 pub fn load(
120 workspace: WeakView<Workspace>,
121 cx: AsyncWindowContext,
122 ) -> Task<Result<View<Self>>> {
123 cx.spawn(|mut cx| async move {
124 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
125 let saved_conversations = SavedConversationMetadata::list(fs.clone())
126 .await
127 .log_err()
128 .unwrap_or_default();
129
130 // TODO: deserialize state.
131 let workspace_handle = workspace.clone();
132 workspace.update(&mut cx, |workspace, cx| {
133 cx.new_view::<Self>(|cx| {
134 const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
135 let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move {
136 let mut events = fs
137 .watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION)
138 .await;
139 while events.next().await.is_some() {
140 let saved_conversations = SavedConversationMetadata::list(fs.clone())
141 .await
142 .log_err()
143 .unwrap_or_default();
144 this.update(&mut cx, |this, cx| {
145 this.saved_conversations = saved_conversations;
146 cx.notify();
147 })
148 .ok();
149 }
150
151 anyhow::Ok(())
152 });
153
154 let toolbar = cx.new_view(|cx| {
155 let mut toolbar = Toolbar::new();
156 toolbar.set_can_navigate(false, cx);
157 toolbar.add_item(cx.new_view(BufferSearchBar::new), cx);
158 toolbar
159 });
160
161 let focus_handle = cx.focus_handle();
162 let subscriptions = vec![
163 cx.on_focus_in(&focus_handle, Self::focus_in),
164 cx.on_focus_out(&focus_handle, Self::focus_out),
165 cx.observe_global::<CompletionProvider>({
166 let mut prev_settings_version =
167 CompletionProvider::global(cx).settings_version();
168 move |this, cx| {
169 this.completion_provider_changed(prev_settings_version, cx);
170 prev_settings_version =
171 CompletionProvider::global(cx).settings_version();
172 }
173 }),
174 ];
175 let model = CompletionProvider::global(cx).default_model();
176
177 cx.observe_global::<FileIcons>(|_, cx| {
178 cx.notify();
179 })
180 .detach();
181
182 Self {
183 workspace: workspace_handle,
184 active_conversation_editor: None,
185 show_saved_conversations: false,
186 saved_conversations,
187 saved_conversations_scroll_handle: Default::default(),
188 zoomed: false,
189 focus_handle,
190 toolbar,
191 languages: workspace.app_state().languages.clone(),
192 fs: workspace.app_state().fs.clone(),
193 telemetry: workspace.client().telemetry().clone(),
194 width: None,
195 height: None,
196 _subscriptions: subscriptions,
197 next_inline_assist_id: 0,
198 pending_inline_assists: Default::default(),
199 pending_inline_assist_ids_by_editor: Default::default(),
200 include_conversation_in_next_inline_assist: false,
201 inline_prompt_history: Default::default(),
202 _watch_saved_conversations,
203 model,
204 authentication_prompt: None,
205 }
206 })
207 })
208 })
209 }
210
211 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
212 self.toolbar
213 .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
214 cx.notify();
215 if self.focus_handle.is_focused(cx) {
216 if let Some(editor) = self.active_conversation_editor() {
217 cx.focus_view(editor);
218 }
219 }
220 }
221
222 fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
223 self.toolbar
224 .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx));
225 cx.notify();
226 }
227
228 fn completion_provider_changed(
229 &mut self,
230 prev_settings_version: usize,
231 cx: &mut ViewContext<Self>,
232 ) {
233 if self.is_authenticated(cx) {
234 self.authentication_prompt = None;
235
236 let model = CompletionProvider::global(cx).default_model();
237 self.set_model(model, cx);
238
239 if self.active_conversation_editor().is_none() {
240 self.new_conversation(cx);
241 }
242 } else if self.authentication_prompt.is_none()
243 || prev_settings_version != CompletionProvider::global(cx).settings_version()
244 {
245 self.authentication_prompt =
246 Some(cx.update_global::<CompletionProvider, _>(|provider, cx| {
247 provider.authentication_prompt(cx)
248 }));
249 }
250 }
251
252 pub fn inline_assist(
253 workspace: &mut Workspace,
254 _: &InlineAssist,
255 cx: &mut ViewContext<Workspace>,
256 ) {
257 let settings = AssistantSettings::get_global(cx);
258 if !settings.enabled {
259 return;
260 }
261
262 let Some(assistant) = workspace.panel::<AssistantPanel>(cx) else {
263 return;
264 };
265
266 let conversation_editor =
267 assistant
268 .read(cx)
269 .active_conversation_editor()
270 .and_then(|editor| {
271 let editor = &editor.read(cx).editor;
272 if editor.read(cx).is_focused(cx) {
273 Some(editor.clone())
274 } else {
275 None
276 }
277 });
278
279 let show_include_conversation;
280 let active_editor;
281 if let Some(conversation_editor) = conversation_editor {
282 active_editor = conversation_editor;
283 show_include_conversation = false;
284 } else if let Some(workspace_editor) = workspace
285 .active_item(cx)
286 .and_then(|item| item.act_as::<Editor>(cx))
287 {
288 active_editor = workspace_editor;
289 show_include_conversation = true;
290 } else {
291 return;
292 };
293 let project = workspace.project().clone();
294
295 if assistant.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
296 assistant.update(cx, |assistant, cx| {
297 assistant.new_inline_assist(&active_editor, &project, show_include_conversation, cx)
298 });
299 } else {
300 let assistant = assistant.downgrade();
301 cx.spawn(|workspace, mut cx| async move {
302 assistant
303 .update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
304 .await?;
305 if assistant.update(&mut cx, |assistant, cx| assistant.is_authenticated(cx))? {
306 assistant.update(&mut cx, |assistant, cx| {
307 assistant.new_inline_assist(
308 &active_editor,
309 &project,
310 show_include_conversation,
311 cx,
312 )
313 })?;
314 } else {
315 workspace.update(&mut cx, |workspace, cx| {
316 workspace.focus_panel::<AssistantPanel>(cx)
317 })?;
318 }
319
320 anyhow::Ok(())
321 })
322 .detach_and_log_err(cx)
323 }
324 }
325
326 fn new_inline_assist(
327 &mut self,
328 editor: &View<Editor>,
329 project: &Model<Project>,
330 show_include_conversation: bool,
331 cx: &mut ViewContext<Self>,
332 ) {
333 let selection = editor.read(cx).selections.newest_anchor().clone();
334 if selection.start.excerpt_id != selection.end.excerpt_id {
335 return;
336 }
337 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
338
339 // Extend the selection to the start and the end of the line.
340 let mut point_selection = selection.map(|selection| selection.to_point(&snapshot));
341 if point_selection.end > point_selection.start {
342 point_selection.start.column = 0;
343 // If the selection ends at the start of the line, we don't want to include it.
344 if point_selection.end.column == 0 {
345 point_selection.end.row -= 1;
346 }
347 point_selection.end.column = snapshot.line_len(MultiBufferRow(point_selection.end.row));
348 }
349
350 let codegen_kind = if point_selection.start == point_selection.end {
351 CodegenKind::Generate {
352 position: snapshot.anchor_after(point_selection.start),
353 }
354 } else {
355 CodegenKind::Transform {
356 range: snapshot.anchor_before(point_selection.start)
357 ..snapshot.anchor_after(point_selection.end),
358 }
359 };
360
361 let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
362 let telemetry = self.telemetry.clone();
363
364 let codegen = cx.new_model(|cx| {
365 Codegen::new(
366 editor.read(cx).buffer().clone(),
367 codegen_kind,
368 Some(telemetry),
369 cx,
370 )
371 });
372
373 let measurements = Arc::new(Mutex::new(BlockMeasurements::default()));
374 let inline_assistant = cx.new_view(|cx| {
375 InlineAssistant::new(
376 inline_assist_id,
377 measurements.clone(),
378 show_include_conversation,
379 show_include_conversation && self.include_conversation_in_next_inline_assist,
380 self.inline_prompt_history.clone(),
381 codegen.clone(),
382 cx,
383 )
384 });
385 let block_id = editor.update(cx, |editor, cx| {
386 editor.change_selections(None, cx, |selections| {
387 selections.select_anchor_ranges([selection.head()..selection.head()])
388 });
389 editor.insert_blocks(
390 [BlockProperties {
391 style: BlockStyle::Flex,
392 position: snapshot.anchor_before(Point::new(point_selection.head().row, 0)),
393 height: 2,
394 render: Box::new({
395 let inline_assistant = inline_assistant.clone();
396 move |cx: &mut BlockContext| {
397 *measurements.lock() = BlockMeasurements {
398 anchor_x: cx.anchor_x,
399 gutter_width: cx.gutter_dimensions.width,
400 };
401 inline_assistant.clone().into_any_element()
402 }
403 }),
404 disposition: if selection.reversed {
405 BlockDisposition::Above
406 } else {
407 BlockDisposition::Below
408 },
409 }],
410 Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)),
411 cx,
412 )[0]
413 });
414
415 self.pending_inline_assists.insert(
416 inline_assist_id,
417 PendingInlineAssist {
418 editor: editor.downgrade(),
419 inline_assistant: Some((block_id, inline_assistant.clone())),
420 codegen: codegen.clone(),
421 project: project.downgrade(),
422 _subscriptions: vec![
423 cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event),
424 cx.subscribe(editor, {
425 let inline_assistant = inline_assistant.downgrade();
426 move |_, editor, event, cx| {
427 if let Some(inline_assistant) = inline_assistant.upgrade() {
428 if let EditorEvent::SelectionsChanged { local } = event {
429 if *local
430 && inline_assistant.focus_handle(cx).contains_focused(cx)
431 {
432 cx.focus_view(&editor);
433 }
434 }
435 }
436 }
437 }),
438 cx.observe(&codegen, {
439 let editor = editor.downgrade();
440 move |this, _, cx| {
441 if let Some(editor) = editor.upgrade() {
442 this.update_highlights_for_editor(&editor, cx);
443 }
444 }
445 }),
446 cx.subscribe(&codegen, move |this, codegen, event, cx| match event {
447 codegen::Event::Undone => {
448 this.finish_inline_assist(inline_assist_id, false, cx)
449 }
450 codegen::Event::Finished => {
451 let pending_assist = if let Some(pending_assist) =
452 this.pending_inline_assists.get(&inline_assist_id)
453 {
454 pending_assist
455 } else {
456 return;
457 };
458
459 let error = codegen
460 .read(cx)
461 .error()
462 .map(|error| format!("Inline assistant error: {}", error));
463 if let Some(error) = error {
464 if pending_assist.inline_assistant.is_none() {
465 if let Some(workspace) = this.workspace.upgrade() {
466 workspace.update(cx, |workspace, cx| {
467 struct InlineAssistantError;
468
469 let id =
470 NotificationId::identified::<InlineAssistantError>(
471 inline_assist_id,
472 );
473
474 workspace.show_toast(Toast::new(id, error), cx);
475 })
476 }
477
478 this.finish_inline_assist(inline_assist_id, false, cx);
479 }
480 } else {
481 this.finish_inline_assist(inline_assist_id, false, cx);
482 }
483 }
484 }),
485 ],
486 },
487 );
488 self.pending_inline_assist_ids_by_editor
489 .entry(editor.downgrade())
490 .or_default()
491 .push(inline_assist_id);
492 self.update_highlights_for_editor(editor, cx);
493 }
494
495 fn handle_inline_assistant_event(
496 &mut self,
497 inline_assistant: View<InlineAssistant>,
498 event: &InlineAssistantEvent,
499 cx: &mut ViewContext<Self>,
500 ) {
501 let assist_id = inline_assistant.read(cx).id;
502 match event {
503 InlineAssistantEvent::Confirmed {
504 prompt,
505 include_conversation,
506 } => {
507 self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx);
508 }
509 InlineAssistantEvent::Canceled => {
510 self.finish_inline_assist(assist_id, true, cx);
511 }
512 InlineAssistantEvent::Dismissed => {
513 self.hide_inline_assist(assist_id, cx);
514 }
515 InlineAssistantEvent::IncludeConversationToggled {
516 include_conversation,
517 } => {
518 self.include_conversation_in_next_inline_assist = *include_conversation;
519 }
520 }
521 }
522
523 fn cancel_last_inline_assist(
524 workspace: &mut Workspace,
525 _: &editor::actions::Cancel,
526 cx: &mut ViewContext<Workspace>,
527 ) {
528 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
529 if let Some(editor) = workspace
530 .active_item(cx)
531 .and_then(|item| item.downcast::<Editor>())
532 {
533 let handled = panel.update(cx, |panel, cx| {
534 if let Some(assist_id) = panel
535 .pending_inline_assist_ids_by_editor
536 .get(&editor.downgrade())
537 .and_then(|assist_ids| assist_ids.last().copied())
538 {
539 panel.finish_inline_assist(assist_id, true, cx);
540 true
541 } else {
542 false
543 }
544 });
545 if handled {
546 return;
547 }
548 }
549 }
550
551 cx.propagate();
552 }
553
554 fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext<Self>) {
555 self.hide_inline_assist(assist_id, cx);
556
557 if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) {
558 if let hash_map::Entry::Occupied(mut entry) = self
559 .pending_inline_assist_ids_by_editor
560 .entry(pending_assist.editor.clone())
561 {
562 entry.get_mut().retain(|id| *id != assist_id);
563 if entry.get().is_empty() {
564 entry.remove();
565 }
566 }
567
568 if let Some(editor) = pending_assist.editor.upgrade() {
569 self.update_highlights_for_editor(&editor, cx);
570
571 if undo {
572 pending_assist
573 .codegen
574 .update(cx, |codegen, cx| codegen.undo(cx));
575 }
576 }
577 }
578 }
579
580 fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext<Self>) {
581 if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) {
582 if let Some(editor) = pending_assist.editor.upgrade() {
583 if let Some((block_id, inline_assistant)) = pending_assist.inline_assistant.take() {
584 editor.update(cx, |editor, cx| {
585 editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
586 if inline_assistant.focus_handle(cx).contains_focused(cx) {
587 editor.focus(cx);
588 }
589 });
590 }
591 }
592 }
593 }
594
595 fn confirm_inline_assist(
596 &mut self,
597 inline_assist_id: usize,
598 user_prompt: &str,
599 include_conversation: bool,
600 cx: &mut ViewContext<Self>,
601 ) {
602 let conversation = if include_conversation {
603 self.active_conversation_editor()
604 .map(|editor| editor.read(cx).conversation.clone())
605 } else {
606 None
607 };
608
609 let pending_assist =
610 if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) {
611 pending_assist
612 } else {
613 return;
614 };
615
616 let editor = if let Some(editor) = pending_assist.editor.upgrade() {
617 editor
618 } else {
619 return;
620 };
621
622 let project = pending_assist.project.clone();
623
624 let project_name = project.upgrade().map(|project| {
625 project
626 .read(cx)
627 .worktree_root_names(cx)
628 .collect::<Vec<&str>>()
629 .join("/")
630 });
631
632 self.inline_prompt_history
633 .retain(|prompt| prompt != user_prompt);
634 self.inline_prompt_history.push_back(user_prompt.into());
635 if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN {
636 self.inline_prompt_history.pop_front();
637 }
638
639 let codegen = pending_assist.codegen.clone();
640 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
641 let range = codegen.read(cx).range();
642 let start = snapshot.point_to_buffer_offset(range.start);
643 let end = snapshot.point_to_buffer_offset(range.end);
644 let (buffer, range) = if let Some((start, end)) = start.zip(end) {
645 let (start_buffer, start_buffer_offset) = start;
646 let (end_buffer, end_buffer_offset) = end;
647 if start_buffer.remote_id() == end_buffer.remote_id() {
648 (start_buffer.clone(), start_buffer_offset..end_buffer_offset)
649 } else {
650 self.finish_inline_assist(inline_assist_id, false, cx);
651 return;
652 }
653 } else {
654 self.finish_inline_assist(inline_assist_id, false, cx);
655 return;
656 };
657
658 let language = buffer.language_at(range.start);
659 let language_name = if let Some(language) = language.as_ref() {
660 if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
661 None
662 } else {
663 Some(language.name())
664 }
665 } else {
666 None
667 };
668
669 // Higher Temperature increases the randomness of model outputs.
670 // If Markdown or No Language is Known, increase the randomness for more creative output
671 // If Code, decrease temperature to get more deterministic outputs
672 let temperature = if let Some(language) = language_name.clone() {
673 if language.as_ref() == "Markdown" {
674 1.0
675 } else {
676 0.5
677 }
678 } else {
679 1.0
680 };
681
682 let user_prompt = user_prompt.to_string();
683
684 let prompt = cx.background_executor().spawn(async move {
685 let language_name = language_name.as_deref();
686 generate_content_prompt(user_prompt, language_name, buffer, range, project_name)
687 });
688
689 let mut messages = Vec::new();
690 if let Some(conversation) = conversation {
691 let conversation = conversation.read(cx);
692 let buffer = conversation.buffer.read(cx);
693 messages.extend(
694 conversation
695 .messages(cx)
696 .map(|message| message.to_request_message(buffer)),
697 );
698 }
699 let model = self.model.clone();
700
701 cx.spawn(|_, mut cx| async move {
702 // I Don't know if we want to return a ? here.
703 let prompt = prompt.await?;
704
705 messages.push(LanguageModelRequestMessage {
706 role: Role::User,
707 content: prompt,
708 });
709
710 let request = LanguageModelRequest {
711 model,
712 messages,
713 stop: vec!["|END|>".to_string()],
714 temperature,
715 };
716
717 codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?;
718 anyhow::Ok(())
719 })
720 .detach();
721 }
722
723 fn update_highlights_for_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Self>) {
724 let mut background_ranges = Vec::new();
725 let mut foreground_ranges = Vec::new();
726 let empty_inline_assist_ids = Vec::new();
727 let inline_assist_ids = self
728 .pending_inline_assist_ids_by_editor
729 .get(&editor.downgrade())
730 .unwrap_or(&empty_inline_assist_ids);
731
732 for inline_assist_id in inline_assist_ids {
733 if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) {
734 let codegen = pending_assist.codegen.read(cx);
735 background_ranges.push(codegen.range());
736 foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
737 }
738 }
739
740 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
741 merge_ranges(&mut background_ranges, &snapshot);
742 merge_ranges(&mut foreground_ranges, &snapshot);
743 editor.update(cx, |editor, cx| {
744 if background_ranges.is_empty() {
745 editor.clear_background_highlights::<PendingInlineAssist>(cx);
746 } else {
747 editor.highlight_background::<PendingInlineAssist>(
748 &background_ranges,
749 |theme| theme.editor_active_line_background, // TODO use the appropriate color
750 cx,
751 );
752 }
753
754 if foreground_ranges.is_empty() {
755 editor.clear_highlights::<PendingInlineAssist>(cx);
756 } else {
757 editor.highlight_text::<PendingInlineAssist>(
758 foreground_ranges,
759 HighlightStyle {
760 fade_out: Some(0.6),
761 ..Default::default()
762 },
763 cx,
764 );
765 }
766 });
767 }
768
769 fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ConversationEditor>> {
770 let workspace = self.workspace.upgrade()?;
771
772 let editor = cx.new_view(|cx| {
773 ConversationEditor::new(
774 self.model.clone(),
775 self.languages.clone(),
776 self.fs.clone(),
777 workspace,
778 cx,
779 )
780 });
781
782 self.show_conversation(editor.clone(), cx);
783 Some(editor)
784 }
785
786 fn show_conversation(
787 &mut self,
788 conversation_editor: View<ConversationEditor>,
789 cx: &mut ViewContext<Self>,
790 ) {
791 let mut subscriptions = Vec::new();
792 subscriptions
793 .push(cx.subscribe(&conversation_editor, Self::handle_conversation_editor_event));
794
795 let conversation = conversation_editor.read(cx).conversation.clone();
796 subscriptions.push(cx.observe(&conversation, |_, _, cx| cx.notify()));
797
798 let editor = conversation_editor.read(cx).editor.clone();
799 self.toolbar.update(cx, |toolbar, cx| {
800 toolbar.set_active_item(Some(&editor), cx);
801 });
802 if self.focus_handle.contains_focused(cx) {
803 cx.focus_view(&editor);
804 }
805 self.active_conversation_editor = Some(ActiveConversationEditor {
806 editor: conversation_editor,
807 _subscriptions: subscriptions,
808 });
809 self.show_saved_conversations = false;
810
811 cx.notify();
812 }
813
814 fn cycle_model(&mut self, cx: &mut ViewContext<Self>) {
815 let next_model = match &self.model {
816 LanguageModel::OpenAi(model) => LanguageModel::OpenAi(match &model {
817 open_ai::Model::ThreePointFiveTurbo => open_ai::Model::Four,
818 open_ai::Model::Four => open_ai::Model::FourTurbo,
819 open_ai::Model::FourTurbo => open_ai::Model::FourOmni,
820 open_ai::Model::FourOmni => open_ai::Model::ThreePointFiveTurbo,
821 }),
822 LanguageModel::Anthropic(model) => LanguageModel::Anthropic(match &model {
823 anthropic::Model::Claude3Opus => anthropic::Model::Claude3Sonnet,
824 anthropic::Model::Claude3Sonnet => anthropic::Model::Claude3Haiku,
825 anthropic::Model::Claude3Haiku => anthropic::Model::Claude3Opus,
826 }),
827 LanguageModel::ZedDotDev(model) => LanguageModel::ZedDotDev(match &model {
828 ZedDotDevModel::Gpt3Point5Turbo => ZedDotDevModel::Gpt4,
829 ZedDotDevModel::Gpt4 => ZedDotDevModel::Gpt4Turbo,
830 ZedDotDevModel::Gpt4Turbo => ZedDotDevModel::Gpt4Omni,
831 ZedDotDevModel::Gpt4Omni => ZedDotDevModel::Claude3Opus,
832 ZedDotDevModel::Claude3Opus => ZedDotDevModel::Claude3Sonnet,
833 ZedDotDevModel::Claude3Sonnet => ZedDotDevModel::Claude3Haiku,
834 ZedDotDevModel::Claude3Haiku => {
835 match CompletionProvider::global(cx).default_model() {
836 LanguageModel::ZedDotDev(custom @ ZedDotDevModel::Custom(_)) => custom,
837 _ => ZedDotDevModel::Gpt3Point5Turbo,
838 }
839 }
840 ZedDotDevModel::Custom(_) => ZedDotDevModel::Gpt3Point5Turbo,
841 }),
842 };
843
844 self.set_model(next_model, cx);
845 }
846
847 fn set_model(&mut self, model: LanguageModel, cx: &mut ViewContext<Self>) {
848 self.model = model.clone();
849 if let Some(editor) = self.active_conversation_editor() {
850 editor.update(cx, |active_conversation, cx| {
851 active_conversation
852 .conversation
853 .update(cx, |conversation, cx| {
854 conversation.set_model(model, cx);
855 })
856 })
857 }
858 cx.notify();
859 }
860
861 fn handle_conversation_editor_event(
862 &mut self,
863 _: View<ConversationEditor>,
864 event: &ConversationEditorEvent,
865 cx: &mut ViewContext<Self>,
866 ) {
867 match event {
868 ConversationEditorEvent::TabContentChanged => cx.notify(),
869 }
870 }
871
872 fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext<Self>) {
873 if self.zoomed {
874 cx.emit(PanelEvent::ZoomOut)
875 } else {
876 cx.emit(PanelEvent::ZoomIn)
877 }
878 }
879
880 fn toggle_history(&mut self, _: &ToggleHistory, cx: &mut ViewContext<Self>) {
881 self.show_saved_conversations = !self.show_saved_conversations;
882 cx.notify();
883 }
884
885 fn show_history(&mut self, cx: &mut ViewContext<Self>) {
886 if !self.show_saved_conversations {
887 self.show_saved_conversations = true;
888 cx.notify();
889 }
890 }
891
892 fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext<Self>) {
893 let mut propagate = true;
894 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
895 search_bar.update(cx, |search_bar, cx| {
896 if search_bar.show(cx) {
897 search_bar.search_suggested(cx);
898 if action.focus {
899 let focus_handle = search_bar.focus_handle(cx);
900 search_bar.select_query(cx);
901 cx.focus(&focus_handle);
902 }
903 propagate = false
904 }
905 });
906 }
907 if propagate {
908 cx.propagate();
909 }
910 }
911
912 fn handle_editor_cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
913 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
914 if !search_bar.read(cx).is_dismissed() {
915 search_bar.update(cx, |search_bar, cx| {
916 search_bar.dismiss(&Default::default(), cx)
917 });
918 return;
919 }
920 }
921 cx.propagate();
922 }
923
924 fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext<Self>) {
925 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
926 search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx));
927 }
928 }
929
930 fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext<Self>) {
931 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
932 search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx));
933 }
934 }
935
936 fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
937 CompletionProvider::global(cx)
938 .reset_credentials(cx)
939 .detach_and_log_err(cx);
940 }
941
942 fn active_conversation_editor(&self) -> Option<&View<ConversationEditor>> {
943 Some(&self.active_conversation_editor.as_ref()?.editor)
944 }
945
946 fn render_popover_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
947 let assistant = cx.view().clone();
948 let zoomed = self.zoomed;
949 popover_menu("assistant-popover")
950 .trigger(IconButton::new("trigger", IconName::Menu))
951 .menu(move |cx| {
952 let assistant = assistant.clone();
953 ContextMenu::build(cx, |menu, _cx| {
954 menu.entry(
955 if zoomed { "Zoom Out" } else { "Zoom In" },
956 Some(Box::new(ToggleZoom)),
957 {
958 let assistant = assistant.clone();
959 move |cx| {
960 assistant.focus_handle(cx).dispatch_action(&ToggleZoom, cx);
961 }
962 },
963 )
964 .entry("New Context", Some(Box::new(NewFile)), {
965 let assistant = assistant.clone();
966 move |cx| {
967 assistant.focus_handle(cx).dispatch_action(&NewFile, cx);
968 }
969 })
970 .entry("History", Some(Box::new(ToggleHistory)), {
971 let assistant = assistant.clone();
972 move |cx| assistant.update(cx, |assistant, cx| assistant.show_history(cx))
973 })
974 })
975 .into()
976 })
977 }
978
979 fn render_inject_context_menu(&self, _cx: &mut ViewContext<Self>) -> impl Element {
980 let workspace = self.workspace.clone();
981
982 popover_menu("inject-context-menu")
983 .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
984 // Tooltip::with_meta("Insert Context", None, "Type # to insert via keyboard", cx)
985 Tooltip::text("Insert Context", cx)
986 }))
987 .menu(move |cx| {
988 ContextMenu::build(cx, |menu, _cx| {
989 // menu.entry("Insert Search", None, {
990 // let assistant = assistant.clone();
991 // move |_cx| {}
992 // })
993 // .entry("Insert Docs", None, {
994 // let assistant = assistant.clone();
995 // move |cx| {}
996 // })
997 menu.entry("Quote Selection", None, {
998 let workspace = workspace.clone();
999 move |cx| {
1000 workspace
1001 .update(cx, |workspace, cx| {
1002 ConversationEditor::quote_selection(
1003 workspace,
1004 &Default::default(),
1005 cx,
1006 )
1007 })
1008 .ok();
1009 }
1010 })
1011 })
1012 .into()
1013 })
1014 }
1015
1016 fn render_assist_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
1017 IconButton::new("assist_button", IconName::MagicWand)
1018 .on_click(cx.listener(|this, _event, cx| {
1019 if let Some(active_editor) = this.active_conversation_editor() {
1020 active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
1021 }
1022 }))
1023 .icon_size(IconSize::Small)
1024 .tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx))
1025 }
1026
1027 fn render_saved_conversation(
1028 &mut self,
1029 index: usize,
1030 cx: &mut ViewContext<Self>,
1031 ) -> impl IntoElement {
1032 let conversation = &self.saved_conversations[index];
1033 let path = conversation.path.clone();
1034
1035 ButtonLike::new(index)
1036 .on_click(cx.listener(move |this, _, cx| {
1037 this.open_conversation(path.clone(), cx)
1038 .detach_and_log_err(cx)
1039 }))
1040 .full_width()
1041 .child(
1042 div()
1043 .flex()
1044 .w_full()
1045 .gap_2()
1046 .child(
1047 Label::new(conversation.mtime.format("%F %I:%M%p").to_string())
1048 .color(Color::Muted)
1049 .size(LabelSize::Small),
1050 )
1051 .child(Label::new(conversation.title.clone()).size(LabelSize::Small)),
1052 )
1053 }
1054
1055 fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
1056 cx.focus(&self.focus_handle);
1057
1058 let fs = self.fs.clone();
1059 let workspace = self.workspace.clone();
1060 let languages = self.languages.clone();
1061 let telemetry = self.telemetry.clone();
1062 cx.spawn(|this, mut cx| async move {
1063 let saved_conversation = SavedConversation::load(&path, fs.as_ref()).await?;
1064 let model = this.update(&mut cx, |this, _| this.model.clone())?;
1065 let conversation = Conversation::deserialize(
1066 saved_conversation,
1067 model,
1068 path.clone(),
1069 languages,
1070 Some(telemetry),
1071 &mut cx,
1072 )
1073 .await?;
1074
1075 this.update(&mut cx, |this, cx| {
1076 let workspace = workspace
1077 .upgrade()
1078 .ok_or_else(|| anyhow!("workspace dropped"))?;
1079 let editor = cx.new_view(|cx| {
1080 ConversationEditor::for_conversation(conversation, fs, workspace, cx)
1081 });
1082 this.show_conversation(editor, cx);
1083 anyhow::Ok(())
1084 })??;
1085 Ok(())
1086 })
1087 }
1088
1089 fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
1090 CompletionProvider::global(cx).is_authenticated()
1091 }
1092
1093 fn authenticate(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
1094 cx.update_global::<CompletionProvider, _>(|provider, cx| provider.authenticate(cx))
1095 }
1096
1097 fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1098 let header = TabBar::new("assistant_header")
1099 .start_child(h_flex().gap_1().child(self.render_popover_button(cx)))
1100 .children(self.active_conversation_editor().map(|editor| {
1101 h_flex()
1102 .h(rems(Tab::CONTAINER_HEIGHT_IN_REMS))
1103 .flex_1()
1104 .px_2()
1105 .child(Label::new(editor.read(cx).title(cx)).into_element())
1106 }))
1107 .end_child(
1108 h_flex()
1109 .gap_2()
1110 .when_some(self.active_conversation_editor(), |this, editor| {
1111 let conversation = editor.read(cx).conversation.clone();
1112 this.child(
1113 h_flex()
1114 .gap_1()
1115 .child(self.render_model(&conversation, cx))
1116 .children(self.render_remaining_tokens(&conversation, cx)),
1117 )
1118 .child(
1119 ui::Divider::vertical()
1120 .inset()
1121 .color(ui::DividerColor::Border),
1122 )
1123 })
1124 .child(
1125 h_flex()
1126 .gap_1()
1127 .child(self.render_inject_context_menu(cx))
1128 .child(Self::render_assist_button(cx)),
1129 ),
1130 );
1131
1132 let contents = if self.active_conversation_editor().is_some() {
1133 let mut registrar = DivRegistrar::new(
1134 |panel, cx| panel.toolbar.read(cx).item_of_type::<BufferSearchBar>(),
1135 cx,
1136 );
1137 BufferSearchBar::register(&mut registrar);
1138 registrar.into_div()
1139 } else {
1140 div()
1141 };
1142
1143 v_flex()
1144 .key_context("AssistantPanel")
1145 .size_full()
1146 .on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
1147 this.new_conversation(cx);
1148 }))
1149 .on_action(cx.listener(AssistantPanel::toggle_zoom))
1150 .on_action(cx.listener(AssistantPanel::toggle_history))
1151 .on_action(cx.listener(AssistantPanel::deploy))
1152 .on_action(cx.listener(AssistantPanel::select_next_match))
1153 .on_action(cx.listener(AssistantPanel::select_prev_match))
1154 .on_action(cx.listener(AssistantPanel::handle_editor_cancel))
1155 .on_action(cx.listener(AssistantPanel::reset_credentials))
1156 .track_focus(&self.focus_handle)
1157 .child(header)
1158 .children(if self.toolbar.read(cx).hidden() {
1159 None
1160 } else {
1161 Some(self.toolbar.clone())
1162 })
1163 .child(contents.flex_1().child(
1164 if self.show_saved_conversations || self.active_conversation_editor().is_none() {
1165 let view = cx.view().clone();
1166 let scroll_handle = self.saved_conversations_scroll_handle.clone();
1167 let conversation_count = self.saved_conversations.len();
1168 canvas(
1169 move |bounds, cx| {
1170 let mut saved_conversations = uniform_list(
1171 view,
1172 "saved_conversations",
1173 conversation_count,
1174 |this, range, cx| {
1175 range
1176 .map(|ix| this.render_saved_conversation(ix, cx))
1177 .collect()
1178 },
1179 )
1180 .track_scroll(scroll_handle)
1181 .into_any_element();
1182 saved_conversations.prepaint_as_root(
1183 bounds.origin,
1184 bounds.size.map(AvailableSpace::Definite),
1185 cx,
1186 );
1187 saved_conversations
1188 },
1189 |_bounds, mut saved_conversations, cx| saved_conversations.paint(cx),
1190 )
1191 .size_full()
1192 .into_any_element()
1193 } else if let Some(editor) = self.active_conversation_editor() {
1194 let editor = editor.clone();
1195 div().size_full().child(editor.clone()).into_any_element()
1196 } else {
1197 div().into_any_element()
1198 },
1199 ))
1200 }
1201
1202 fn render_model(
1203 &self,
1204 conversation: &Model<Conversation>,
1205 cx: &mut ViewContext<Self>,
1206 ) -> impl IntoElement {
1207 Button::new("current_model", conversation.read(cx).model.display_name())
1208 .style(ButtonStyle::Filled)
1209 .tooltip(move |cx| Tooltip::text("Change Model", cx))
1210 .on_click(cx.listener(|this, _, cx| this.cycle_model(cx)))
1211 }
1212
1213 fn render_remaining_tokens(
1214 &self,
1215 conversation: &Model<Conversation>,
1216 cx: &mut ViewContext<Self>,
1217 ) -> Option<impl IntoElement> {
1218 let remaining_tokens = conversation.read(cx).remaining_tokens()?;
1219 let remaining_tokens_color = if remaining_tokens <= 0 {
1220 Color::Error
1221 } else if remaining_tokens <= 500 {
1222 Color::Warning
1223 } else {
1224 Color::Muted
1225 };
1226 Some(
1227 Label::new(remaining_tokens.to_string())
1228 .size(LabelSize::Small)
1229 .color(remaining_tokens_color),
1230 )
1231 }
1232}
1233
1234impl Render for AssistantPanel {
1235 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1236 if let Some(authentication_prompt) = self.authentication_prompt.as_ref() {
1237 authentication_prompt.clone().into_any()
1238 } else {
1239 self.render_signed_in(cx).into_any_element()
1240 }
1241 }
1242}
1243
1244impl Panel for AssistantPanel {
1245 fn persistent_name() -> &'static str {
1246 "AssistantPanel"
1247 }
1248
1249 fn position(&self, cx: &WindowContext) -> DockPosition {
1250 match AssistantSettings::get_global(cx).dock {
1251 AssistantDockPosition::Left => DockPosition::Left,
1252 AssistantDockPosition::Bottom => DockPosition::Bottom,
1253 AssistantDockPosition::Right => DockPosition::Right,
1254 }
1255 }
1256
1257 fn position_is_valid(&self, _: DockPosition) -> bool {
1258 true
1259 }
1260
1261 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1262 settings::update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings| {
1263 let dock = match position {
1264 DockPosition::Left => AssistantDockPosition::Left,
1265 DockPosition::Bottom => AssistantDockPosition::Bottom,
1266 DockPosition::Right => AssistantDockPosition::Right,
1267 };
1268 settings.set_dock(dock);
1269 });
1270 }
1271
1272 fn size(&self, cx: &WindowContext) -> Pixels {
1273 let settings = AssistantSettings::get_global(cx);
1274 match self.position(cx) {
1275 DockPosition::Left | DockPosition::Right => {
1276 self.width.unwrap_or(settings.default_width)
1277 }
1278 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1279 }
1280 }
1281
1282 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1283 match self.position(cx) {
1284 DockPosition::Left | DockPosition::Right => self.width = size,
1285 DockPosition::Bottom => self.height = size,
1286 }
1287 cx.notify();
1288 }
1289
1290 fn is_zoomed(&self, _: &WindowContext) -> bool {
1291 self.zoomed
1292 }
1293
1294 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1295 self.zoomed = zoomed;
1296 cx.notify();
1297 }
1298
1299 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1300 if active {
1301 let load_credentials = self.authenticate(cx);
1302 cx.spawn(|this, mut cx| async move {
1303 load_credentials.await?;
1304 this.update(&mut cx, |this, cx| {
1305 if this.is_authenticated(cx) && this.active_conversation_editor().is_none() {
1306 this.new_conversation(cx);
1307 }
1308 })
1309 })
1310 .detach_and_log_err(cx);
1311 }
1312 }
1313
1314 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1315 let settings = AssistantSettings::get_global(cx);
1316 if !settings.enabled || !settings.button {
1317 return None;
1318 }
1319
1320 Some(IconName::Ai)
1321 }
1322
1323 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1324 Some("Assistant Panel")
1325 }
1326
1327 fn toggle_action(&self) -> Box<dyn Action> {
1328 Box::new(ToggleFocus)
1329 }
1330}
1331
1332impl EventEmitter<PanelEvent> for AssistantPanel {}
1333
1334impl FocusableView for AssistantPanel {
1335 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1336 self.focus_handle.clone()
1337 }
1338}
1339
1340enum ConversationEvent {
1341 MessagesEdited,
1342 SummaryChanged,
1343 StreamedCompletion,
1344}
1345
1346#[derive(Default)]
1347struct Summary {
1348 text: String,
1349 done: bool,
1350}
1351
1352pub struct Conversation {
1353 id: Option<String>,
1354 buffer: Model<Buffer>,
1355 pub(crate) ambient_context: AmbientContext,
1356 message_anchors: Vec<MessageAnchor>,
1357 messages_metadata: HashMap<MessageId, MessageMetadata>,
1358 next_message_id: MessageId,
1359 summary: Option<Summary>,
1360 pending_summary: Task<Option<()>>,
1361 completion_count: usize,
1362 pending_completions: Vec<PendingCompletion>,
1363 model: LanguageModel,
1364 token_count: Option<usize>,
1365 pending_token_count: Task<Option<()>>,
1366 pending_save: Task<Result<()>>,
1367 path: Option<PathBuf>,
1368 _subscriptions: Vec<Subscription>,
1369 telemetry: Option<Arc<Telemetry>>,
1370}
1371
1372impl EventEmitter<ConversationEvent> for Conversation {}
1373
1374impl Conversation {
1375 fn new(
1376 model: LanguageModel,
1377 language_registry: Arc<LanguageRegistry>,
1378 telemetry: Option<Arc<Telemetry>>,
1379 cx: &mut ModelContext<Self>,
1380 ) -> Self {
1381 let markdown = language_registry.language_for_name("Markdown");
1382 let buffer = cx.new_model(|cx| {
1383 let mut buffer = Buffer::local("", cx);
1384 buffer.set_language_registry(language_registry);
1385 cx.spawn(|buffer, mut cx| async move {
1386 let markdown = markdown.await?;
1387 buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
1388 buffer.set_language(Some(markdown), cx)
1389 })?;
1390 anyhow::Ok(())
1391 })
1392 .detach_and_log_err(cx);
1393 buffer
1394 });
1395
1396 let mut this = Self {
1397 id: Some(Uuid::new_v4().to_string()),
1398 message_anchors: Default::default(),
1399 messages_metadata: Default::default(),
1400 next_message_id: Default::default(),
1401 ambient_context: AmbientContext::default(),
1402 summary: None,
1403 pending_summary: Task::ready(None),
1404 completion_count: Default::default(),
1405 pending_completions: Default::default(),
1406 token_count: None,
1407 pending_token_count: Task::ready(None),
1408 model,
1409 _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
1410 pending_save: Task::ready(Ok(())),
1411 path: None,
1412 buffer,
1413 telemetry,
1414 };
1415
1416 let message = MessageAnchor {
1417 id: MessageId(post_inc(&mut this.next_message_id.0)),
1418 start: language::Anchor::MIN,
1419 };
1420 this.message_anchors.push(message.clone());
1421 this.messages_metadata.insert(
1422 message.id,
1423 MessageMetadata {
1424 role: Role::User,
1425 status: MessageStatus::Done,
1426 },
1427 );
1428
1429 this.count_remaining_tokens(cx);
1430 this
1431 }
1432
1433 fn serialize(&self, cx: &AppContext) -> SavedConversation {
1434 SavedConversation {
1435 id: self.id.clone(),
1436 zed: "conversation".into(),
1437 version: SavedConversation::VERSION.into(),
1438 text: self.buffer.read(cx).text(),
1439 message_metadata: self.messages_metadata.clone(),
1440 messages: self
1441 .messages(cx)
1442 .map(|message| SavedMessage {
1443 id: message.id,
1444 start: message.offset_range.start,
1445 })
1446 .collect(),
1447 summary: self
1448 .summary
1449 .as_ref()
1450 .map(|summary| summary.text.clone())
1451 .unwrap_or_default(),
1452 }
1453 }
1454
1455 async fn deserialize(
1456 saved_conversation: SavedConversation,
1457 model: LanguageModel,
1458 path: PathBuf,
1459 language_registry: Arc<LanguageRegistry>,
1460 telemetry: Option<Arc<Telemetry>>,
1461 cx: &mut AsyncAppContext,
1462 ) -> Result<Model<Self>> {
1463 let id = match saved_conversation.id {
1464 Some(id) => Some(id),
1465 None => Some(Uuid::new_v4().to_string()),
1466 };
1467
1468 let markdown = language_registry.language_for_name("Markdown");
1469 let mut message_anchors = Vec::new();
1470 let mut next_message_id = MessageId(0);
1471 let buffer = cx.new_model(|cx| {
1472 let mut buffer = Buffer::local(saved_conversation.text, cx);
1473 for message in saved_conversation.messages {
1474 message_anchors.push(MessageAnchor {
1475 id: message.id,
1476 start: buffer.anchor_before(message.start),
1477 });
1478 next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1));
1479 }
1480 buffer.set_language_registry(language_registry);
1481 cx.spawn(|buffer, mut cx| async move {
1482 let markdown = markdown.await?;
1483 buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
1484 buffer.set_language(Some(markdown), cx)
1485 })?;
1486 anyhow::Ok(())
1487 })
1488 .detach_and_log_err(cx);
1489 buffer
1490 })?;
1491
1492 cx.new_model(|cx| {
1493 let mut this = Self {
1494 id,
1495 message_anchors,
1496 messages_metadata: saved_conversation.message_metadata,
1497 next_message_id,
1498 ambient_context: AmbientContext::default(),
1499 summary: Some(Summary {
1500 text: saved_conversation.summary,
1501 done: true,
1502 }),
1503 pending_summary: Task::ready(None),
1504 completion_count: Default::default(),
1505 pending_completions: Default::default(),
1506 token_count: None,
1507 pending_token_count: Task::ready(None),
1508 model,
1509 _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
1510 pending_save: Task::ready(Ok(())),
1511 path: Some(path),
1512 buffer,
1513 telemetry,
1514 };
1515 this.count_remaining_tokens(cx);
1516 this
1517 })
1518 }
1519
1520 fn toggle_recent_buffers(&mut self, cx: &mut ModelContext<Self>) {
1521 self.ambient_context.recent_buffers.enabled = !self.ambient_context.recent_buffers.enabled;
1522 self.update_recent_buffers_context(cx);
1523 }
1524
1525 fn toggle_current_project_context(
1526 &mut self,
1527 fs: Arc<dyn Fs>,
1528 project: WeakModel<Project>,
1529 cx: &mut ModelContext<Self>,
1530 ) {
1531 self.ambient_context.current_project.enabled =
1532 !self.ambient_context.current_project.enabled;
1533 self.ambient_context.current_project.update(fs, project, cx);
1534 }
1535
1536 fn set_recent_buffers(
1537 &mut self,
1538 buffers: impl IntoIterator<Item = Model<Buffer>>,
1539 cx: &mut ModelContext<Self>,
1540 ) {
1541 self.ambient_context.recent_buffers.buffers.clear();
1542 self.ambient_context
1543 .recent_buffers
1544 .buffers
1545 .extend(buffers.into_iter().map(|buffer| RecentBuffer {
1546 buffer: buffer.downgrade(),
1547 _subscription: cx.observe(&buffer, |this, _, cx| {
1548 this.update_recent_buffers_context(cx);
1549 }),
1550 }));
1551 self.update_recent_buffers_context(cx);
1552 }
1553
1554 fn update_recent_buffers_context(&mut self, cx: &mut ModelContext<Self>) {
1555 let buffers = self
1556 .ambient_context
1557 .recent_buffers
1558 .buffers
1559 .iter()
1560 .filter_map(|recent| {
1561 recent
1562 .buffer
1563 .read_with(cx, |buffer, cx| {
1564 (
1565 buffer.file().map(|file| file.full_path(cx)),
1566 buffer.snapshot(),
1567 )
1568 })
1569 .ok()
1570 })
1571 .collect::<Vec<_>>();
1572
1573 if !self.ambient_context.recent_buffers.enabled || buffers.is_empty() {
1574 self.ambient_context.recent_buffers.message.clear();
1575 self.ambient_context.recent_buffers.pending_message = None;
1576 self.count_remaining_tokens(cx);
1577 cx.notify();
1578 } else {
1579 self.ambient_context.recent_buffers.pending_message =
1580 Some(cx.spawn(|this, mut cx| async move {
1581 const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
1582 cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
1583
1584 let message = cx
1585 .background_executor()
1586 .spawn(async move { Self::message_for_recent_buffers(&buffers) })
1587 .await;
1588 this.update(&mut cx, |this, cx| {
1589 this.ambient_context.recent_buffers.message = message;
1590 this.count_remaining_tokens(cx);
1591 cx.notify();
1592 })
1593 .ok();
1594 }));
1595 }
1596 }
1597
1598 fn message_for_recent_buffers(buffers: &[(Option<PathBuf>, BufferSnapshot)]) -> String {
1599 let mut message = String::new();
1600 writeln!(
1601 message,
1602 "The following is a list of recent buffers that the user has opened."
1603 )
1604 .unwrap();
1605 writeln!(
1606 message,
1607 "For every line in the buffer, I will include a row number that line corresponds to."
1608 )
1609 .unwrap();
1610 writeln!(
1611 message,
1612 "Lines that don't have a number correspond to errors and warnings. For example:"
1613 )
1614 .unwrap();
1615 writeln!(message, "path/to/file.md").unwrap();
1616 writeln!(message, "```markdown").unwrap();
1617 writeln!(message, "1 The quick brown fox").unwrap();
1618 writeln!(message, "2 jumps over one active").unwrap();
1619 writeln!(message, " --- error: should be 'the'").unwrap();
1620 writeln!(message, " ------ error: should be 'lazy'").unwrap();
1621 writeln!(message, "3 dog").unwrap();
1622 writeln!(message, "```").unwrap();
1623
1624 message.push('\n');
1625 writeln!(message, "Here's the actual recent buffer list:").unwrap();
1626 for (path, buffer) in buffers {
1627 if let Some(path) = path {
1628 writeln!(message, "{}", path.display()).unwrap();
1629 } else {
1630 writeln!(message, "untitled").unwrap();
1631 }
1632
1633 if let Some(language) = buffer.language() {
1634 writeln!(message, "```{}", language.name().to_lowercase()).unwrap();
1635 } else {
1636 writeln!(message, "```").unwrap();
1637 }
1638
1639 let mut diagnostics = buffer
1640 .diagnostics_in_range::<_, Point>(
1641 language::Anchor::MIN..language::Anchor::MAX,
1642 false,
1643 )
1644 .peekable();
1645
1646 let mut active_diagnostics = Vec::<DiagnosticEntry<Point>>::new();
1647 const GUTTER_PADDING: usize = 4;
1648 let gutter_width =
1649 ((buffer.max_point().row + 1) as f32).log10() as usize + 1 + GUTTER_PADDING;
1650 for buffer_row in 0..=buffer.max_point().row {
1651 let display_row = buffer_row + 1;
1652 active_diagnostics.retain(|diagnostic| {
1653 (diagnostic.range.start.row..=diagnostic.range.end.row).contains(&buffer_row)
1654 });
1655 while diagnostics.peek().map_or(false, |diagnostic| {
1656 (diagnostic.range.start.row..=diagnostic.range.end.row).contains(&buffer_row)
1657 }) {
1658 active_diagnostics.push(diagnostics.next().unwrap());
1659 }
1660
1661 let row_width = (display_row as f32).log10() as usize + 1;
1662 write!(message, "{}", display_row).unwrap();
1663 if row_width < gutter_width {
1664 message.extend(iter::repeat(' ').take(gutter_width - row_width));
1665 }
1666
1667 for chunk in buffer.text_for_range(
1668 Point::new(buffer_row, 0)..Point::new(buffer_row, buffer.line_len(buffer_row)),
1669 ) {
1670 message.push_str(chunk);
1671 }
1672 message.push('\n');
1673
1674 for diagnostic in &active_diagnostics {
1675 message.extend(iter::repeat(' ').take(gutter_width));
1676
1677 let start_column = if diagnostic.range.start.row == buffer_row {
1678 message
1679 .extend(iter::repeat(' ').take(diagnostic.range.start.column as usize));
1680 diagnostic.range.start.column
1681 } else {
1682 0
1683 };
1684 let end_column = if diagnostic.range.end.row == buffer_row {
1685 diagnostic.range.end.column
1686 } else {
1687 buffer.line_len(buffer_row)
1688 };
1689
1690 message.extend(iter::repeat('-').take((end_column - start_column) as usize));
1691 writeln!(message, " {}", diagnostic.diagnostic.message).unwrap();
1692 }
1693 }
1694
1695 message.push('\n');
1696 }
1697
1698 writeln!(
1699 message,
1700 "When quoting the above code, mention which rows the code occurs at."
1701 )
1702 .unwrap();
1703 writeln!(
1704 message,
1705 "Never include rows in the quoted code itself and only report lines that didn't start with a row number."
1706 )
1707 .unwrap();
1708
1709 message
1710 }
1711
1712 fn handle_buffer_event(
1713 &mut self,
1714 _: Model<Buffer>,
1715 event: &language::Event,
1716 cx: &mut ModelContext<Self>,
1717 ) {
1718 if *event == language::Event::Edited {
1719 self.count_remaining_tokens(cx);
1720 cx.emit(ConversationEvent::MessagesEdited);
1721 }
1722 }
1723
1724 pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
1725 let request = self.to_completion_request(cx);
1726 self.pending_token_count = cx.spawn(|this, mut cx| {
1727 async move {
1728 cx.background_executor()
1729 .timer(Duration::from_millis(200))
1730 .await;
1731
1732 let token_count = cx
1733 .update(|cx| CompletionProvider::global(cx).count_tokens(request, cx))?
1734 .await?;
1735
1736 this.update(&mut cx, |this, cx| {
1737 this.token_count = Some(token_count);
1738 cx.notify()
1739 })?;
1740 anyhow::Ok(())
1741 }
1742 .log_err()
1743 });
1744 }
1745
1746 fn remaining_tokens(&self) -> Option<isize> {
1747 Some(self.model.max_token_count() as isize - self.token_count? as isize)
1748 }
1749
1750 fn set_model(&mut self, model: LanguageModel, cx: &mut ModelContext<Self>) {
1751 self.model = model;
1752 self.count_remaining_tokens(cx);
1753 }
1754
1755 fn assist(
1756 &mut self,
1757 selected_messages: HashSet<MessageId>,
1758 cx: &mut ModelContext<Self>,
1759 ) -> Vec<MessageAnchor> {
1760 let mut user_messages = Vec::new();
1761
1762 let last_message_id = if let Some(last_message_id) =
1763 self.message_anchors.iter().rev().find_map(|message| {
1764 message
1765 .start
1766 .is_valid(self.buffer.read(cx))
1767 .then_some(message.id)
1768 }) {
1769 last_message_id
1770 } else {
1771 return Default::default();
1772 };
1773
1774 let mut should_assist = false;
1775 for selected_message_id in selected_messages {
1776 let selected_message_role =
1777 if let Some(metadata) = self.messages_metadata.get(&selected_message_id) {
1778 metadata.role
1779 } else {
1780 continue;
1781 };
1782
1783 if selected_message_role == Role::Assistant {
1784 if let Some(user_message) = self.insert_message_after(
1785 selected_message_id,
1786 Role::User,
1787 MessageStatus::Done,
1788 cx,
1789 ) {
1790 user_messages.push(user_message);
1791 }
1792 } else {
1793 should_assist = true;
1794 }
1795 }
1796
1797 if should_assist {
1798 if !CompletionProvider::global(cx).is_authenticated() {
1799 log::info!("completion provider has no credentials");
1800 return Default::default();
1801 }
1802
1803 let request = self.to_completion_request(cx);
1804 let stream = CompletionProvider::global(cx).complete(request);
1805 let assistant_message = self
1806 .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
1807 .unwrap();
1808
1809 // Queue up the user's next reply.
1810 let user_message = self
1811 .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx)
1812 .unwrap();
1813 user_messages.push(user_message);
1814
1815 let task = cx.spawn({
1816 |this, mut cx| async move {
1817 let assistant_message_id = assistant_message.id;
1818 let mut response_latency = None;
1819 let stream_completion = async {
1820 let request_start = Instant::now();
1821 let mut messages = stream.await?;
1822
1823 while let Some(message) = messages.next().await {
1824 if response_latency.is_none() {
1825 response_latency = Some(request_start.elapsed());
1826 }
1827 let text = message?;
1828
1829 this.update(&mut cx, |this, cx| {
1830 let message_ix = this
1831 .message_anchors
1832 .iter()
1833 .position(|message| message.id == assistant_message_id)?;
1834 this.buffer.update(cx, |buffer, cx| {
1835 let offset = this.message_anchors[message_ix + 1..]
1836 .iter()
1837 .find(|message| message.start.is_valid(buffer))
1838 .map_or(buffer.len(), |message| {
1839 message.start.to_offset(buffer).saturating_sub(1)
1840 });
1841 buffer.edit([(offset..offset, text)], None, cx);
1842 });
1843 cx.emit(ConversationEvent::StreamedCompletion);
1844
1845 Some(())
1846 })?;
1847 smol::future::yield_now().await;
1848 }
1849
1850 this.update(&mut cx, |this, cx| {
1851 this.pending_completions
1852 .retain(|completion| completion.id != this.completion_count);
1853 this.summarize(cx);
1854 })?;
1855
1856 anyhow::Ok(())
1857 };
1858
1859 let result = stream_completion.await;
1860
1861 this.update(&mut cx, |this, cx| {
1862 if let Some(metadata) =
1863 this.messages_metadata.get_mut(&assistant_message.id)
1864 {
1865 let error_message = result
1866 .err()
1867 .map(|error| error.to_string().trim().to_string());
1868 if let Some(error_message) = error_message.as_ref() {
1869 metadata.status =
1870 MessageStatus::Error(SharedString::from(error_message.clone()));
1871 } else {
1872 metadata.status = MessageStatus::Done;
1873 }
1874
1875 if let Some(telemetry) = this.telemetry.as_ref() {
1876 telemetry.report_assistant_event(
1877 this.id.clone(),
1878 AssistantKind::Panel,
1879 this.model.telemetry_id(),
1880 response_latency,
1881 error_message,
1882 );
1883 }
1884
1885 cx.emit(ConversationEvent::MessagesEdited);
1886 }
1887 })
1888 .ok();
1889 }
1890 });
1891
1892 self.pending_completions.push(PendingCompletion {
1893 id: post_inc(&mut self.completion_count),
1894 _task: task,
1895 });
1896 }
1897
1898 user_messages
1899 }
1900
1901 fn to_completion_request(&self, cx: &mut ModelContext<Conversation>) -> LanguageModelRequest {
1902 let recent_buffers_context = self.ambient_context.recent_buffers.to_message();
1903 let current_project_context = self.ambient_context.current_project.to_message();
1904
1905 let messages = recent_buffers_context
1906 .into_iter()
1907 .chain(current_project_context)
1908 .chain(
1909 self.messages(cx)
1910 .filter(|message| matches!(message.status, MessageStatus::Done))
1911 .map(|message| message.to_request_message(self.buffer.read(cx))),
1912 );
1913
1914 LanguageModelRequest {
1915 model: self.model.clone(),
1916 messages: messages.collect(),
1917 stop: vec![],
1918 temperature: 1.0,
1919 }
1920 }
1921
1922 fn cancel_last_assist(&mut self) -> bool {
1923 self.pending_completions.pop().is_some()
1924 }
1925
1926 fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
1927 for id in ids {
1928 if let Some(metadata) = self.messages_metadata.get_mut(&id) {
1929 metadata.role.cycle();
1930 cx.emit(ConversationEvent::MessagesEdited);
1931 cx.notify();
1932 }
1933 }
1934 }
1935
1936 fn insert_message_after(
1937 &mut self,
1938 message_id: MessageId,
1939 role: Role,
1940 status: MessageStatus,
1941 cx: &mut ModelContext<Self>,
1942 ) -> Option<MessageAnchor> {
1943 if let Some(prev_message_ix) = self
1944 .message_anchors
1945 .iter()
1946 .position(|message| message.id == message_id)
1947 {
1948 // Find the next valid message after the one we were given.
1949 let mut next_message_ix = prev_message_ix + 1;
1950 while let Some(next_message) = self.message_anchors.get(next_message_ix) {
1951 if next_message.start.is_valid(self.buffer.read(cx)) {
1952 break;
1953 }
1954 next_message_ix += 1;
1955 }
1956
1957 let start = self.buffer.update(cx, |buffer, cx| {
1958 let offset = self
1959 .message_anchors
1960 .get(next_message_ix)
1961 .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1);
1962 buffer.edit([(offset..offset, "\n")], None, cx);
1963 buffer.anchor_before(offset + 1)
1964 });
1965 let message = MessageAnchor {
1966 id: MessageId(post_inc(&mut self.next_message_id.0)),
1967 start,
1968 };
1969 self.message_anchors
1970 .insert(next_message_ix, message.clone());
1971 self.messages_metadata
1972 .insert(message.id, MessageMetadata { role, status });
1973 cx.emit(ConversationEvent::MessagesEdited);
1974 Some(message)
1975 } else {
1976 None
1977 }
1978 }
1979
1980 fn split_message(
1981 &mut self,
1982 range: Range<usize>,
1983 cx: &mut ModelContext<Self>,
1984 ) -> (Option<MessageAnchor>, Option<MessageAnchor>) {
1985 let start_message = self.message_for_offset(range.start, cx);
1986 let end_message = self.message_for_offset(range.end, cx);
1987 if let Some((start_message, end_message)) = start_message.zip(end_message) {
1988 // Prevent splitting when range spans multiple messages.
1989 if start_message.id != end_message.id {
1990 return (None, None);
1991 }
1992
1993 let message = start_message;
1994 let role = message.role;
1995 let mut edited_buffer = false;
1996
1997 let mut suffix_start = None;
1998 if range.start > message.offset_range.start && range.end < message.offset_range.end - 1
1999 {
2000 if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') {
2001 suffix_start = Some(range.end + 1);
2002 } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') {
2003 suffix_start = Some(range.end);
2004 }
2005 }
2006
2007 let suffix = if let Some(suffix_start) = suffix_start {
2008 MessageAnchor {
2009 id: MessageId(post_inc(&mut self.next_message_id.0)),
2010 start: self.buffer.read(cx).anchor_before(suffix_start),
2011 }
2012 } else {
2013 self.buffer.update(cx, |buffer, cx| {
2014 buffer.edit([(range.end..range.end, "\n")], None, cx);
2015 });
2016 edited_buffer = true;
2017 MessageAnchor {
2018 id: MessageId(post_inc(&mut self.next_message_id.0)),
2019 start: self.buffer.read(cx).anchor_before(range.end + 1),
2020 }
2021 };
2022
2023 self.message_anchors
2024 .insert(message.index_range.end + 1, suffix.clone());
2025 self.messages_metadata.insert(
2026 suffix.id,
2027 MessageMetadata {
2028 role,
2029 status: MessageStatus::Done,
2030 },
2031 );
2032
2033 let new_messages =
2034 if range.start == range.end || range.start == message.offset_range.start {
2035 (None, Some(suffix))
2036 } else {
2037 let mut prefix_end = None;
2038 if range.start > message.offset_range.start
2039 && range.end < message.offset_range.end - 1
2040 {
2041 if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') {
2042 prefix_end = Some(range.start + 1);
2043 } else if self.buffer.read(cx).reversed_chars_at(range.start).next()
2044 == Some('\n')
2045 {
2046 prefix_end = Some(range.start);
2047 }
2048 }
2049
2050 let selection = if let Some(prefix_end) = prefix_end {
2051 cx.emit(ConversationEvent::MessagesEdited);
2052 MessageAnchor {
2053 id: MessageId(post_inc(&mut self.next_message_id.0)),
2054 start: self.buffer.read(cx).anchor_before(prefix_end),
2055 }
2056 } else {
2057 self.buffer.update(cx, |buffer, cx| {
2058 buffer.edit([(range.start..range.start, "\n")], None, cx)
2059 });
2060 edited_buffer = true;
2061 MessageAnchor {
2062 id: MessageId(post_inc(&mut self.next_message_id.0)),
2063 start: self.buffer.read(cx).anchor_before(range.end + 1),
2064 }
2065 };
2066
2067 self.message_anchors
2068 .insert(message.index_range.end + 1, selection.clone());
2069 self.messages_metadata.insert(
2070 selection.id,
2071 MessageMetadata {
2072 role,
2073 status: MessageStatus::Done,
2074 },
2075 );
2076 (Some(selection), Some(suffix))
2077 };
2078
2079 if !edited_buffer {
2080 cx.emit(ConversationEvent::MessagesEdited);
2081 }
2082 new_messages
2083 } else {
2084 (None, None)
2085 }
2086 }
2087
2088 fn summarize(&mut self, cx: &mut ModelContext<Self>) {
2089 if self.message_anchors.len() >= 2 && self.summary.is_none() {
2090 if !CompletionProvider::global(cx).is_authenticated() {
2091 return;
2092 }
2093
2094 let messages = self
2095 .messages(cx)
2096 .take(2)
2097 .map(|message| message.to_request_message(self.buffer.read(cx)))
2098 .chain(Some(LanguageModelRequestMessage {
2099 role: Role::User,
2100 content: "Summarize the conversation into a short title without punctuation"
2101 .into(),
2102 }));
2103 let request = LanguageModelRequest {
2104 model: self.model.clone(),
2105 messages: messages.collect(),
2106 stop: vec![],
2107 temperature: 1.0,
2108 };
2109
2110 let stream = CompletionProvider::global(cx).complete(request);
2111 self.pending_summary = cx.spawn(|this, mut cx| {
2112 async move {
2113 let mut messages = stream.await?;
2114
2115 while let Some(message) = messages.next().await {
2116 let text = message?;
2117 this.update(&mut cx, |this, cx| {
2118 this.summary
2119 .get_or_insert(Default::default())
2120 .text
2121 .push_str(&text);
2122 cx.emit(ConversationEvent::SummaryChanged);
2123 })?;
2124 }
2125
2126 this.update(&mut cx, |this, cx| {
2127 if let Some(summary) = this.summary.as_mut() {
2128 summary.done = true;
2129 cx.emit(ConversationEvent::SummaryChanged);
2130 }
2131 })?;
2132
2133 anyhow::Ok(())
2134 }
2135 .log_err()
2136 });
2137 }
2138 }
2139
2140 fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option<Message> {
2141 self.messages_for_offsets([offset], cx).pop()
2142 }
2143
2144 fn messages_for_offsets(
2145 &self,
2146 offsets: impl IntoIterator<Item = usize>,
2147 cx: &AppContext,
2148 ) -> Vec<Message> {
2149 let mut result = Vec::new();
2150
2151 let mut messages = self.messages(cx).peekable();
2152 let mut offsets = offsets.into_iter().peekable();
2153 let mut current_message = messages.next();
2154 while let Some(offset) = offsets.next() {
2155 // Locate the message that contains the offset.
2156 while current_message.as_ref().map_or(false, |message| {
2157 !message.offset_range.contains(&offset) && messages.peek().is_some()
2158 }) {
2159 current_message = messages.next();
2160 }
2161 let Some(message) = current_message.as_ref() else {
2162 break;
2163 };
2164
2165 // Skip offsets that are in the same message.
2166 while offsets.peek().map_or(false, |offset| {
2167 message.offset_range.contains(offset) || messages.peek().is_none()
2168 }) {
2169 offsets.next();
2170 }
2171
2172 result.push(message.clone());
2173 }
2174 result
2175 }
2176
2177 fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Message> {
2178 let buffer = self.buffer.read(cx);
2179 let mut message_anchors = self.message_anchors.iter().enumerate().peekable();
2180 iter::from_fn(move || {
2181 if let Some((start_ix, message_anchor)) = message_anchors.next() {
2182 let metadata = self.messages_metadata.get(&message_anchor.id)?;
2183 let message_start = message_anchor.start.to_offset(buffer);
2184 let mut message_end = None;
2185 let mut end_ix = start_ix;
2186 while let Some((_, next_message)) = message_anchors.peek() {
2187 if next_message.start.is_valid(buffer) {
2188 message_end = Some(next_message.start);
2189 break;
2190 } else {
2191 end_ix += 1;
2192 message_anchors.next();
2193 }
2194 }
2195 let message_end = message_end
2196 .unwrap_or(language::Anchor::MAX)
2197 .to_offset(buffer);
2198 return Some(Message {
2199 index_range: start_ix..end_ix,
2200 offset_range: message_start..message_end,
2201 id: message_anchor.id,
2202 anchor: message_anchor.start,
2203 role: metadata.role,
2204 status: metadata.status.clone(),
2205 });
2206 }
2207 None
2208 })
2209 }
2210
2211 fn save(
2212 &mut self,
2213 debounce: Option<Duration>,
2214 fs: Arc<dyn Fs>,
2215 cx: &mut ModelContext<Conversation>,
2216 ) {
2217 self.pending_save = cx.spawn(|this, mut cx| async move {
2218 if let Some(debounce) = debounce {
2219 cx.background_executor().timer(debounce).await;
2220 }
2221
2222 let (old_path, summary) = this.read_with(&cx, |this, _| {
2223 let path = this.path.clone();
2224 let summary = if let Some(summary) = this.summary.as_ref() {
2225 if summary.done {
2226 Some(summary.text.clone())
2227 } else {
2228 None
2229 }
2230 } else {
2231 None
2232 };
2233 (path, summary)
2234 })?;
2235
2236 if let Some(summary) = summary {
2237 let conversation = this.read_with(&cx, |this, cx| this.serialize(cx))?;
2238 let path = if let Some(old_path) = old_path {
2239 old_path
2240 } else {
2241 let mut discriminant = 1;
2242 let mut new_path;
2243 loop {
2244 new_path = CONVERSATIONS_DIR.join(&format!(
2245 "{} - {}.zed.json",
2246 summary.trim(),
2247 discriminant
2248 ));
2249 if fs.is_file(&new_path).await {
2250 discriminant += 1;
2251 } else {
2252 break;
2253 }
2254 }
2255 new_path
2256 };
2257
2258 fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?;
2259 fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap())
2260 .await?;
2261 this.update(&mut cx, |this, _| this.path = Some(path))?;
2262 }
2263
2264 Ok(())
2265 });
2266 }
2267}
2268
2269struct PendingCompletion {
2270 id: usize,
2271 _task: Task<()>,
2272}
2273
2274enum ConversationEditorEvent {
2275 TabContentChanged,
2276}
2277
2278#[derive(Copy, Clone, Debug, PartialEq)]
2279struct ScrollPosition {
2280 offset_before_cursor: gpui::Point<f32>,
2281 cursor: Anchor,
2282}
2283
2284struct ConversationEditor {
2285 conversation: Model<Conversation>,
2286 fs: Arc<dyn Fs>,
2287 workspace: WeakView<Workspace>,
2288 editor: View<Editor>,
2289 blocks: HashSet<BlockId>,
2290 scroll_position: Option<ScrollPosition>,
2291 _subscriptions: Vec<Subscription>,
2292}
2293
2294impl ConversationEditor {
2295 fn new(
2296 model: LanguageModel,
2297 language_registry: Arc<LanguageRegistry>,
2298 fs: Arc<dyn Fs>,
2299 workspace: View<Workspace>,
2300 cx: &mut ViewContext<Self>,
2301 ) -> Self {
2302 let telemetry = workspace.read(cx).client().telemetry().clone();
2303 let conversation =
2304 cx.new_model(|cx| Conversation::new(model, language_registry, Some(telemetry), cx));
2305 Self::for_conversation(conversation, fs, workspace, cx)
2306 }
2307
2308 fn for_conversation(
2309 conversation: Model<Conversation>,
2310 fs: Arc<dyn Fs>,
2311 workspace: View<Workspace>,
2312 cx: &mut ViewContext<Self>,
2313 ) -> Self {
2314 let editor = cx.new_view(|cx| {
2315 let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx);
2316 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
2317 editor.set_show_gutter(false, cx);
2318 editor.set_show_wrap_guides(false, cx);
2319 editor
2320 });
2321
2322 let _subscriptions = vec![
2323 cx.observe(&conversation, |_, _, cx| cx.notify()),
2324 cx.subscribe(&conversation, Self::handle_conversation_event),
2325 cx.subscribe(&editor, Self::handle_editor_event),
2326 cx.subscribe(&workspace, Self::handle_workspace_event),
2327 ];
2328
2329 let mut this = Self {
2330 conversation,
2331 editor,
2332 blocks: Default::default(),
2333 scroll_position: None,
2334 fs,
2335 workspace: workspace.downgrade(),
2336 _subscriptions,
2337 };
2338 this.update_recent_editors(cx);
2339 this.update_message_headers(cx);
2340 this
2341 }
2342
2343 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
2344 let cursors = self.cursors(cx);
2345
2346 let user_messages = self.conversation.update(cx, |conversation, cx| {
2347 let selected_messages = conversation
2348 .messages_for_offsets(cursors, cx)
2349 .into_iter()
2350 .map(|message| message.id)
2351 .collect();
2352 conversation.assist(selected_messages, cx)
2353 });
2354 let new_selections = user_messages
2355 .iter()
2356 .map(|message| {
2357 let cursor = message
2358 .start
2359 .to_offset(self.conversation.read(cx).buffer.read(cx));
2360 cursor..cursor
2361 })
2362 .collect::<Vec<_>>();
2363 if !new_selections.is_empty() {
2364 self.editor.update(cx, |editor, cx| {
2365 editor.change_selections(
2366 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
2367 cx,
2368 |selections| selections.select_ranges(new_selections),
2369 );
2370 });
2371 // Avoid scrolling to the new cursor position so the assistant's output is stable.
2372 cx.defer(|this, _| this.scroll_position = None);
2373 }
2374 }
2375
2376 fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
2377 if !self
2378 .conversation
2379 .update(cx, |conversation, _| conversation.cancel_last_assist())
2380 {
2381 cx.propagate();
2382 }
2383 }
2384
2385 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
2386 let cursors = self.cursors(cx);
2387 self.conversation.update(cx, |conversation, cx| {
2388 let messages = conversation
2389 .messages_for_offsets(cursors, cx)
2390 .into_iter()
2391 .map(|message| message.id)
2392 .collect();
2393 conversation.cycle_message_roles(messages, cx)
2394 });
2395 }
2396
2397 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
2398 let selections = self.editor.read(cx).selections.all::<usize>(cx);
2399 selections
2400 .into_iter()
2401 .map(|selection| selection.head())
2402 .collect()
2403 }
2404
2405 fn handle_conversation_event(
2406 &mut self,
2407 _: Model<Conversation>,
2408 event: &ConversationEvent,
2409 cx: &mut ViewContext<Self>,
2410 ) {
2411 match event {
2412 ConversationEvent::MessagesEdited => {
2413 self.update_message_headers(cx);
2414 self.conversation.update(cx, |conversation, cx| {
2415 conversation.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2416 });
2417 }
2418 ConversationEvent::SummaryChanged => {
2419 cx.emit(ConversationEditorEvent::TabContentChanged);
2420 self.conversation.update(cx, |conversation, cx| {
2421 conversation.save(None, self.fs.clone(), cx);
2422 });
2423 }
2424 ConversationEvent::StreamedCompletion => {
2425 self.editor.update(cx, |editor, cx| {
2426 if let Some(scroll_position) = self.scroll_position {
2427 let snapshot = editor.snapshot(cx);
2428 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
2429 let scroll_top =
2430 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
2431 editor.set_scroll_position(
2432 point(scroll_position.offset_before_cursor.x, scroll_top),
2433 cx,
2434 );
2435 }
2436 });
2437 }
2438 }
2439 }
2440
2441 fn handle_editor_event(
2442 &mut self,
2443 _: View<Editor>,
2444 event: &EditorEvent,
2445 cx: &mut ViewContext<Self>,
2446 ) {
2447 match event {
2448 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
2449 let cursor_scroll_position = self.cursor_scroll_position(cx);
2450 if *autoscroll {
2451 self.scroll_position = cursor_scroll_position;
2452 } else if self.scroll_position != cursor_scroll_position {
2453 self.scroll_position = None;
2454 }
2455 }
2456 EditorEvent::SelectionsChanged { .. } => {
2457 self.scroll_position = self.cursor_scroll_position(cx);
2458 }
2459 _ => {}
2460 }
2461 }
2462
2463 fn handle_workspace_event(
2464 &mut self,
2465 _: View<Workspace>,
2466 event: &WorkspaceEvent,
2467 cx: &mut ViewContext<Self>,
2468 ) {
2469 match event {
2470 WorkspaceEvent::ActiveItemChanged
2471 | WorkspaceEvent::ItemAdded
2472 | WorkspaceEvent::ItemRemoved
2473 | WorkspaceEvent::PaneAdded(_)
2474 | WorkspaceEvent::PaneRemoved => self.update_recent_editors(cx),
2475 _ => {}
2476 }
2477 }
2478
2479 fn update_recent_editors(&mut self, cx: &mut ViewContext<ConversationEditor>) {
2480 let Some(workspace) = self.workspace.upgrade() else {
2481 return;
2482 };
2483
2484 let mut timestamps_by_entity_id = HashMap::default();
2485 for pane in workspace.read(cx).panes() {
2486 let pane = pane.read(cx);
2487 for entry in pane.activation_history() {
2488 timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
2489 }
2490 }
2491
2492 let mut timestamps_by_buffer = HashMap::default();
2493 for editor in workspace.read(cx).items_of_type::<Editor>(cx) {
2494 let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
2495 continue;
2496 };
2497
2498 let new_timestamp = timestamps_by_entity_id
2499 .get(&editor.entity_id())
2500 .copied()
2501 .unwrap_or_default();
2502 let timestamp = timestamps_by_buffer.entry(buffer).or_insert(new_timestamp);
2503 *timestamp = cmp::max(*timestamp, new_timestamp);
2504 }
2505
2506 let mut recent_buffers = timestamps_by_buffer.into_iter().collect::<Vec<_>>();
2507 recent_buffers.sort_unstable_by_key(|(_, timestamp)| *timestamp);
2508 if recent_buffers.len() > MAX_RECENT_BUFFERS {
2509 let excess = recent_buffers.len() - MAX_RECENT_BUFFERS;
2510 recent_buffers.drain(..excess);
2511 }
2512
2513 self.conversation.update(cx, |conversation, cx| {
2514 conversation
2515 .set_recent_buffers(recent_buffers.into_iter().map(|(buffer, _)| buffer), cx);
2516 });
2517 }
2518
2519 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2520 self.editor.update(cx, |editor, cx| {
2521 let snapshot = editor.snapshot(cx);
2522 let cursor = editor.selections.newest_anchor().head();
2523 let cursor_row = cursor
2524 .to_display_point(&snapshot.display_snapshot)
2525 .row()
2526 .as_f32();
2527 let scroll_position = editor
2528 .scroll_manager
2529 .anchor()
2530 .scroll_position(&snapshot.display_snapshot);
2531
2532 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2533 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2534 Some(ScrollPosition {
2535 cursor,
2536 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2537 })
2538 } else {
2539 None
2540 }
2541 })
2542 }
2543
2544 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2545 let project = self
2546 .workspace
2547 .update(cx, |workspace, _cx| workspace.project().downgrade())
2548 .unwrap();
2549
2550 self.editor.update(cx, |editor, cx| {
2551 let buffer = editor.buffer().read(cx).snapshot(cx);
2552 let excerpt_id = *buffer.as_singleton().unwrap().0;
2553 let old_blocks = std::mem::take(&mut self.blocks);
2554 let new_blocks = self
2555 .conversation
2556 .read(cx)
2557 .messages(cx)
2558 .enumerate()
2559 .map(|(ix, message)| BlockProperties {
2560 position: buffer
2561 .anchor_in_excerpt(excerpt_id, message.anchor)
2562 .unwrap(),
2563 height: 2,
2564 style: BlockStyle::Sticky,
2565 render: Box::new({
2566 let fs = self.fs.clone();
2567 let project = project.clone();
2568 let conversation = self.conversation.clone();
2569 move |cx| {
2570 let message_id = message.id;
2571 let sender = ButtonLike::new("role")
2572 .style(ButtonStyle::Filled)
2573 .child(match message.role {
2574 Role::User => Label::new("You").color(Color::Default),
2575 Role::Assistant => Label::new("Assistant").color(Color::Info),
2576 Role::System => Label::new("System").color(Color::Warning),
2577 })
2578 .tooltip(|cx| {
2579 Tooltip::with_meta(
2580 "Toggle message role",
2581 None,
2582 "Available roles: You (User), Assistant, System",
2583 cx,
2584 )
2585 })
2586 .on_click({
2587 let conversation = conversation.clone();
2588 move |_, cx| {
2589 conversation.update(cx, |conversation, cx| {
2590 conversation.cycle_message_roles(
2591 HashSet::from_iter(Some(message_id)),
2592 cx,
2593 )
2594 })
2595 }
2596 });
2597
2598 h_flex()
2599 .id(("message_header", message_id.0))
2600 .h_11()
2601 .w_full()
2602 .relative()
2603 .gap_1()
2604 .child(sender)
2605 .children(
2606 if let MessageStatus::Error(error) = message.status.clone() {
2607 Some(
2608 div()
2609 .id("error")
2610 .tooltip(move |cx| Tooltip::text(error.clone(), cx))
2611 .child(Icon::new(IconName::XCircle)),
2612 )
2613 } else {
2614 None
2615 },
2616 )
2617 .children((ix == 0).then(|| {
2618 div()
2619 .h_flex()
2620 .flex_1()
2621 .justify_end()
2622 .pr_4()
2623 .gap_1()
2624 .child(
2625 IconButton::new("include_file", IconName::File)
2626 .icon_size(IconSize::Small)
2627 .selected(
2628 conversation
2629 .read(cx)
2630 .ambient_context
2631 .recent_buffers
2632 .enabled,
2633 )
2634 .on_click({
2635 let conversation = conversation.downgrade();
2636 move |_, cx| {
2637 conversation
2638 .update(cx, |conversation, cx| {
2639 conversation
2640 .toggle_recent_buffers(cx);
2641 })
2642 .ok();
2643 }
2644 })
2645 .tooltip(|cx| {
2646 Tooltip::text("Include Open Files", cx)
2647 }),
2648 )
2649 .child(
2650 IconButton::new(
2651 "include_current_project",
2652 IconName::FileTree,
2653 )
2654 .icon_size(IconSize::Small)
2655 .selected(
2656 conversation
2657 .read(cx)
2658 .ambient_context
2659 .current_project
2660 .enabled,
2661 )
2662 .on_click({
2663 let fs = fs.clone();
2664 let project = project.clone();
2665 let conversation = conversation.downgrade();
2666 move |_, cx| {
2667 let fs = fs.clone();
2668 let project = project.clone();
2669 conversation
2670 .update(cx, |conversation, cx| {
2671 conversation
2672 .toggle_current_project_context(
2673 fs, project, cx,
2674 );
2675 })
2676 .ok();
2677 }
2678 })
2679 .tooltip(
2680 |cx| Tooltip::text("Include Current Project", cx),
2681 ),
2682 )
2683 .into_any()
2684 }))
2685 .into_any_element()
2686 }
2687 }),
2688 disposition: BlockDisposition::Above,
2689 })
2690 .collect::<Vec<_>>();
2691
2692 editor.remove_blocks(old_blocks, None, cx);
2693 let ids = editor.insert_blocks(new_blocks, None, cx);
2694 self.blocks = HashSet::from_iter(ids);
2695 });
2696 }
2697
2698 fn quote_selection(
2699 workspace: &mut Workspace,
2700 _: &QuoteSelection,
2701 cx: &mut ViewContext<Workspace>,
2702 ) {
2703 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2704 return;
2705 };
2706 let Some(editor) = workspace
2707 .active_item(cx)
2708 .and_then(|item| item.act_as::<Editor>(cx))
2709 else {
2710 return;
2711 };
2712
2713 let editor = editor.read(cx);
2714 let range = editor.selections.newest::<usize>(cx).range();
2715 let buffer = editor.buffer().read(cx).snapshot(cx);
2716 let start_language = buffer.language_at(range.start);
2717 let end_language = buffer.language_at(range.end);
2718 let language_name = if start_language == end_language {
2719 start_language.map(|language| language.code_fence_block_name())
2720 } else {
2721 None
2722 };
2723 let language_name = language_name.as_deref().unwrap_or("");
2724
2725 let selected_text = buffer.text_for_range(range).collect::<String>();
2726 let text = if selected_text.is_empty() {
2727 None
2728 } else {
2729 Some(if language_name == "markdown" {
2730 selected_text
2731 .lines()
2732 .map(|line| format!("> {}", line))
2733 .collect::<Vec<_>>()
2734 .join("\n")
2735 } else {
2736 format!("```{language_name}\n{selected_text}\n```")
2737 })
2738 };
2739
2740 // Activate the panel
2741 if !panel.focus_handle(cx).contains_focused(cx) {
2742 workspace.toggle_panel_focus::<AssistantPanel>(cx);
2743 }
2744
2745 if let Some(text) = text {
2746 panel.update(cx, |panel, cx| {
2747 if let Some(conversation) = panel
2748 .active_conversation_editor()
2749 .cloned()
2750 .or_else(|| panel.new_conversation(cx))
2751 {
2752 conversation.update(cx, |conversation, cx| {
2753 conversation
2754 .editor
2755 .update(cx, |editor, cx| editor.insert(&text, cx))
2756 });
2757 };
2758 });
2759 }
2760 }
2761
2762 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
2763 let editor = self.editor.read(cx);
2764 let conversation = self.conversation.read(cx);
2765 if editor.selections.count() == 1 {
2766 let selection = editor.selections.newest::<usize>(cx);
2767 let mut copied_text = String::new();
2768 let mut spanned_messages = 0;
2769 for message in conversation.messages(cx) {
2770 if message.offset_range.start >= selection.range().end {
2771 break;
2772 } else if message.offset_range.end >= selection.range().start {
2773 let range = cmp::max(message.offset_range.start, selection.range().start)
2774 ..cmp::min(message.offset_range.end, selection.range().end);
2775 if !range.is_empty() {
2776 spanned_messages += 1;
2777 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2778 for chunk in conversation.buffer.read(cx).text_for_range(range) {
2779 copied_text.push_str(chunk);
2780 }
2781 copied_text.push('\n');
2782 }
2783 }
2784 }
2785
2786 if spanned_messages > 1 {
2787 cx.write_to_clipboard(ClipboardItem::new(copied_text));
2788 return;
2789 }
2790 }
2791
2792 cx.propagate();
2793 }
2794
2795 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2796 self.conversation.update(cx, |conversation, cx| {
2797 let selections = self.editor.read(cx).selections.disjoint_anchors();
2798 for selection in selections.as_ref() {
2799 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2800 let range = selection
2801 .map(|endpoint| endpoint.to_offset(&buffer))
2802 .range();
2803 conversation.split_message(range, cx);
2804 }
2805 });
2806 }
2807
2808 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
2809 self.conversation.update(cx, |conversation, cx| {
2810 conversation.save(None, self.fs.clone(), cx)
2811 });
2812 }
2813
2814 fn title(&self, cx: &AppContext) -> String {
2815 self.conversation
2816 .read(cx)
2817 .summary
2818 .as_ref()
2819 .map(|summary| summary.text.clone())
2820 .unwrap_or_else(|| "New Conversation".into())
2821 }
2822}
2823
2824impl EventEmitter<ConversationEditorEvent> for ConversationEditor {}
2825
2826impl Render for ConversationEditor {
2827 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
2828 div()
2829 .key_context("ConversationEditor")
2830 .capture_action(cx.listener(ConversationEditor::cancel_last_assist))
2831 .capture_action(cx.listener(ConversationEditor::save))
2832 .capture_action(cx.listener(ConversationEditor::copy))
2833 .capture_action(cx.listener(ConversationEditor::cycle_message_role))
2834 .on_action(cx.listener(ConversationEditor::assist))
2835 .on_action(cx.listener(ConversationEditor::split))
2836 .size_full()
2837 .v_flex()
2838 .child(
2839 div()
2840 .flex_grow()
2841 .pl_4()
2842 .bg(cx.theme().colors().editor_background)
2843 .child(self.editor.clone()),
2844 )
2845 }
2846}
2847
2848impl FocusableView for ConversationEditor {
2849 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2850 self.editor.focus_handle(cx)
2851 }
2852}
2853
2854#[derive(Clone, Debug)]
2855struct MessageAnchor {
2856 id: MessageId,
2857 start: language::Anchor,
2858}
2859
2860#[derive(Clone, Debug)]
2861pub struct Message {
2862 offset_range: Range<usize>,
2863 index_range: Range<usize>,
2864 id: MessageId,
2865 anchor: language::Anchor,
2866 role: Role,
2867 status: MessageStatus,
2868}
2869
2870impl Message {
2871 fn to_request_message(&self, buffer: &Buffer) -> LanguageModelRequestMessage {
2872 let content = buffer
2873 .text_for_range(self.offset_range.clone())
2874 .collect::<String>();
2875 LanguageModelRequestMessage {
2876 role: self.role,
2877 content: content.trim_end().into(),
2878 }
2879 }
2880}
2881
2882enum InlineAssistantEvent {
2883 Confirmed {
2884 prompt: String,
2885 include_conversation: bool,
2886 },
2887 Canceled,
2888 Dismissed,
2889 IncludeConversationToggled {
2890 include_conversation: bool,
2891 },
2892}
2893
2894struct InlineAssistant {
2895 id: usize,
2896 prompt_editor: View<Editor>,
2897 confirmed: bool,
2898 show_include_conversation: bool,
2899 include_conversation: bool,
2900 measurements: Arc<Mutex<BlockMeasurements>>,
2901 prompt_history: VecDeque<String>,
2902 prompt_history_ix: Option<usize>,
2903 pending_prompt: String,
2904 codegen: Model<Codegen>,
2905 _subscriptions: Vec<Subscription>,
2906}
2907
2908impl EventEmitter<InlineAssistantEvent> for InlineAssistant {}
2909
2910impl Render for InlineAssistant {
2911 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
2912 let measurements = *self.measurements.lock();
2913 h_flex()
2914 .w_full()
2915 .py_2()
2916 .border_y_1()
2917 .border_color(cx.theme().colors().border)
2918 .on_action(cx.listener(Self::confirm))
2919 .on_action(cx.listener(Self::cancel))
2920 .on_action(cx.listener(Self::toggle_include_conversation))
2921 .on_action(cx.listener(Self::move_up))
2922 .on_action(cx.listener(Self::move_down))
2923 .child(
2924 h_flex()
2925 .justify_center()
2926 .w(measurements.gutter_width)
2927 .children(self.show_include_conversation.then(|| {
2928 IconButton::new("include_conversation", IconName::Ai)
2929 .on_click(cx.listener(|this, _, cx| {
2930 this.toggle_include_conversation(&ToggleIncludeConversation, cx)
2931 }))
2932 .selected(self.include_conversation)
2933 .tooltip(|cx| {
2934 Tooltip::for_action(
2935 "Include Conversation",
2936 &ToggleIncludeConversation,
2937 cx,
2938 )
2939 })
2940 }))
2941 .children(if let Some(error) = self.codegen.read(cx).error() {
2942 let error_message = SharedString::from(error.to_string());
2943 Some(
2944 div()
2945 .id("error")
2946 .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
2947 .child(Icon::new(IconName::XCircle).color(Color::Error)),
2948 )
2949 } else {
2950 None
2951 }),
2952 )
2953 .child(
2954 h_flex()
2955 .w_full()
2956 .ml(measurements.anchor_x - measurements.gutter_width)
2957 .child(self.render_prompt_editor(cx)),
2958 )
2959 }
2960}
2961
2962impl FocusableView for InlineAssistant {
2963 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2964 self.prompt_editor.focus_handle(cx)
2965 }
2966}
2967
2968impl InlineAssistant {
2969 #[allow(clippy::too_many_arguments)]
2970 fn new(
2971 id: usize,
2972 measurements: Arc<Mutex<BlockMeasurements>>,
2973 show_include_conversation: bool,
2974 include_conversation: bool,
2975 prompt_history: VecDeque<String>,
2976 codegen: Model<Codegen>,
2977 cx: &mut ViewContext<Self>,
2978 ) -> Self {
2979 let prompt_editor = cx.new_view(|cx| {
2980 let mut editor = Editor::single_line(cx);
2981 let placeholder = match codegen.read(cx).kind() {
2982 CodegenKind::Transform { .. } => "Enter transformation prompt…",
2983 CodegenKind::Generate { .. } => "Enter generation prompt…",
2984 };
2985 editor.set_placeholder_text(placeholder, cx);
2986 editor
2987 });
2988 cx.focus_view(&prompt_editor);
2989
2990 let subscriptions = vec![
2991 cx.observe(&codegen, Self::handle_codegen_changed),
2992 cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
2993 ];
2994
2995 Self {
2996 id,
2997 prompt_editor,
2998 confirmed: false,
2999 show_include_conversation,
3000 include_conversation,
3001 measurements,
3002 prompt_history,
3003 prompt_history_ix: None,
3004 pending_prompt: String::new(),
3005 codegen,
3006 _subscriptions: subscriptions,
3007 }
3008 }
3009
3010 fn handle_prompt_editor_events(
3011 &mut self,
3012 _: View<Editor>,
3013 event: &EditorEvent,
3014 cx: &mut ViewContext<Self>,
3015 ) {
3016 if let EditorEvent::Edited = event {
3017 self.pending_prompt = self.prompt_editor.read(cx).text(cx);
3018 cx.notify();
3019 }
3020 }
3021
3022 fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
3023 let is_read_only = !self.codegen.read(cx).idle();
3024 self.prompt_editor.update(cx, |editor, cx| {
3025 let was_read_only = editor.read_only(cx);
3026 if was_read_only != is_read_only {
3027 if is_read_only {
3028 editor.set_read_only(true);
3029 } else {
3030 self.confirmed = false;
3031 editor.set_read_only(false);
3032 }
3033 }
3034 });
3035 cx.notify();
3036 }
3037
3038 fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
3039 cx.emit(InlineAssistantEvent::Canceled);
3040 }
3041
3042 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
3043 if self.confirmed {
3044 cx.emit(InlineAssistantEvent::Dismissed);
3045 } else {
3046 let prompt = self.prompt_editor.read(cx).text(cx);
3047 self.prompt_editor
3048 .update(cx, |editor, _cx| editor.set_read_only(true));
3049 cx.emit(InlineAssistantEvent::Confirmed {
3050 prompt,
3051 include_conversation: self.include_conversation,
3052 });
3053 self.confirmed = true;
3054 cx.notify();
3055 }
3056 }
3057
3058 fn toggle_include_conversation(
3059 &mut self,
3060 _: &ToggleIncludeConversation,
3061 cx: &mut ViewContext<Self>,
3062 ) {
3063 self.include_conversation = !self.include_conversation;
3064 cx.emit(InlineAssistantEvent::IncludeConversationToggled {
3065 include_conversation: self.include_conversation,
3066 });
3067 cx.notify();
3068 }
3069
3070 fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
3071 if let Some(ix) = self.prompt_history_ix {
3072 if ix > 0 {
3073 self.prompt_history_ix = Some(ix - 1);
3074 let prompt = self.prompt_history[ix - 1].clone();
3075 self.set_prompt(&prompt, cx);
3076 }
3077 } else if !self.prompt_history.is_empty() {
3078 self.prompt_history_ix = Some(self.prompt_history.len() - 1);
3079 let prompt = self.prompt_history[self.prompt_history.len() - 1].clone();
3080 self.set_prompt(&prompt, cx);
3081 }
3082 }
3083
3084 fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
3085 if let Some(ix) = self.prompt_history_ix {
3086 if ix < self.prompt_history.len() - 1 {
3087 self.prompt_history_ix = Some(ix + 1);
3088 let prompt = self.prompt_history[ix + 1].clone();
3089 self.set_prompt(&prompt, cx);
3090 } else {
3091 self.prompt_history_ix = None;
3092 let pending_prompt = self.pending_prompt.clone();
3093 self.set_prompt(&pending_prompt, cx);
3094 }
3095 }
3096 }
3097
3098 fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext<Self>) {
3099 self.prompt_editor.update(cx, |editor, cx| {
3100 editor.buffer().update(cx, |buffer, cx| {
3101 let len = buffer.len(cx);
3102 buffer.edit([(0..len, prompt)], None, cx);
3103 });
3104 });
3105 }
3106
3107 fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3108 let settings = ThemeSettings::get_global(cx);
3109 let text_style = TextStyle {
3110 color: if self.prompt_editor.read(cx).read_only(cx) {
3111 cx.theme().colors().text_disabled
3112 } else {
3113 cx.theme().colors().text
3114 },
3115 font_family: settings.ui_font.family.clone(),
3116 font_features: settings.ui_font.features.clone(),
3117 font_size: rems(0.875).into(),
3118 font_weight: FontWeight::NORMAL,
3119 font_style: FontStyle::Normal,
3120 line_height: relative(1.3),
3121 background_color: None,
3122 underline: None,
3123 strikethrough: None,
3124 white_space: WhiteSpace::Normal,
3125 };
3126 EditorElement::new(
3127 &self.prompt_editor,
3128 EditorStyle {
3129 background: cx.theme().colors().editor_background,
3130 local_player: cx.theme().players().local(),
3131 text: text_style,
3132 ..Default::default()
3133 },
3134 )
3135 }
3136}
3137
3138// This wouldn't need to exist if we could pass parameters when rendering child views.
3139#[derive(Copy, Clone, Default)]
3140struct BlockMeasurements {
3141 anchor_x: Pixels,
3142 gutter_width: Pixels,
3143}
3144
3145struct PendingInlineAssist {
3146 editor: WeakView<Editor>,
3147 inline_assistant: Option<(BlockId, View<InlineAssistant>)>,
3148 codegen: Model<Codegen>,
3149 _subscriptions: Vec<Subscription>,
3150 project: WeakModel<Project>,
3151}
3152
3153fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
3154 ranges.sort_unstable_by(|a, b| {
3155 a.start
3156 .cmp(&b.start, buffer)
3157 .then_with(|| b.end.cmp(&a.end, buffer))
3158 });
3159
3160 let mut ix = 0;
3161 while ix + 1 < ranges.len() {
3162 let b = ranges[ix + 1].clone();
3163 let a = &mut ranges[ix];
3164 if a.end.cmp(&b.start, buffer).is_gt() {
3165 if a.end.cmp(&b.end, buffer).is_lt() {
3166 a.end = b.end;
3167 }
3168 ranges.remove(ix + 1);
3169 } else {
3170 ix += 1;
3171 }
3172 }
3173}
3174
3175#[cfg(test)]
3176mod tests {
3177 use super::*;
3178 use crate::{FakeCompletionProvider, MessageId};
3179 use gpui::{AppContext, TestAppContext};
3180 use settings::SettingsStore;
3181
3182 #[gpui::test]
3183 fn test_inserting_and_removing_messages(cx: &mut AppContext) {
3184 let settings_store = SettingsStore::test(cx);
3185 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3186 cx.set_global(settings_store);
3187 init(cx);
3188 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3189
3190 let conversation =
3191 cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry, None, cx));
3192 let buffer = conversation.read(cx).buffer.clone();
3193
3194 let message_1 = conversation.read(cx).message_anchors[0].clone();
3195 assert_eq!(
3196 messages(&conversation, cx),
3197 vec![(message_1.id, Role::User, 0..0)]
3198 );
3199
3200 let message_2 = conversation.update(cx, |conversation, cx| {
3201 conversation
3202 .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
3203 .unwrap()
3204 });
3205 assert_eq!(
3206 messages(&conversation, cx),
3207 vec![
3208 (message_1.id, Role::User, 0..1),
3209 (message_2.id, Role::Assistant, 1..1)
3210 ]
3211 );
3212
3213 buffer.update(cx, |buffer, cx| {
3214 buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
3215 });
3216 assert_eq!(
3217 messages(&conversation, cx),
3218 vec![
3219 (message_1.id, Role::User, 0..2),
3220 (message_2.id, Role::Assistant, 2..3)
3221 ]
3222 );
3223
3224 let message_3 = conversation.update(cx, |conversation, cx| {
3225 conversation
3226 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3227 .unwrap()
3228 });
3229 assert_eq!(
3230 messages(&conversation, cx),
3231 vec![
3232 (message_1.id, Role::User, 0..2),
3233 (message_2.id, Role::Assistant, 2..4),
3234 (message_3.id, Role::User, 4..4)
3235 ]
3236 );
3237
3238 let message_4 = conversation.update(cx, |conversation, cx| {
3239 conversation
3240 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3241 .unwrap()
3242 });
3243 assert_eq!(
3244 messages(&conversation, cx),
3245 vec![
3246 (message_1.id, Role::User, 0..2),
3247 (message_2.id, Role::Assistant, 2..4),
3248 (message_4.id, Role::User, 4..5),
3249 (message_3.id, Role::User, 5..5),
3250 ]
3251 );
3252
3253 buffer.update(cx, |buffer, cx| {
3254 buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
3255 });
3256 assert_eq!(
3257 messages(&conversation, cx),
3258 vec![
3259 (message_1.id, Role::User, 0..2),
3260 (message_2.id, Role::Assistant, 2..4),
3261 (message_4.id, Role::User, 4..6),
3262 (message_3.id, Role::User, 6..7),
3263 ]
3264 );
3265
3266 // Deleting across message boundaries merges the messages.
3267 buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
3268 assert_eq!(
3269 messages(&conversation, cx),
3270 vec![
3271 (message_1.id, Role::User, 0..3),
3272 (message_3.id, Role::User, 3..4),
3273 ]
3274 );
3275
3276 // Undoing the deletion should also undo the merge.
3277 buffer.update(cx, |buffer, cx| buffer.undo(cx));
3278 assert_eq!(
3279 messages(&conversation, cx),
3280 vec![
3281 (message_1.id, Role::User, 0..2),
3282 (message_2.id, Role::Assistant, 2..4),
3283 (message_4.id, Role::User, 4..6),
3284 (message_3.id, Role::User, 6..7),
3285 ]
3286 );
3287
3288 // Redoing the deletion should also redo the merge.
3289 buffer.update(cx, |buffer, cx| buffer.redo(cx));
3290 assert_eq!(
3291 messages(&conversation, cx),
3292 vec![
3293 (message_1.id, Role::User, 0..3),
3294 (message_3.id, Role::User, 3..4),
3295 ]
3296 );
3297
3298 // Ensure we can still insert after a merged message.
3299 let message_5 = conversation.update(cx, |conversation, cx| {
3300 conversation
3301 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3302 .unwrap()
3303 });
3304 assert_eq!(
3305 messages(&conversation, cx),
3306 vec![
3307 (message_1.id, Role::User, 0..3),
3308 (message_5.id, Role::System, 3..4),
3309 (message_3.id, Role::User, 4..5)
3310 ]
3311 );
3312 }
3313
3314 #[gpui::test]
3315 fn test_message_splitting(cx: &mut AppContext) {
3316 let settings_store = SettingsStore::test(cx);
3317 cx.set_global(settings_store);
3318 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3319 init(cx);
3320 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3321
3322 let conversation =
3323 cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry, None, cx));
3324 let buffer = conversation.read(cx).buffer.clone();
3325
3326 let message_1 = conversation.read(cx).message_anchors[0].clone();
3327 assert_eq!(
3328 messages(&conversation, cx),
3329 vec![(message_1.id, Role::User, 0..0)]
3330 );
3331
3332 buffer.update(cx, |buffer, cx| {
3333 buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
3334 });
3335
3336 let (_, message_2) =
3337 conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx));
3338 let message_2 = message_2.unwrap();
3339
3340 // We recycle newlines in the middle of a split message
3341 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n");
3342 assert_eq!(
3343 messages(&conversation, cx),
3344 vec![
3345 (message_1.id, Role::User, 0..4),
3346 (message_2.id, Role::User, 4..16),
3347 ]
3348 );
3349
3350 let (_, message_3) =
3351 conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx));
3352 let message_3 = message_3.unwrap();
3353
3354 // We don't recycle newlines at the end of a split message
3355 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3356 assert_eq!(
3357 messages(&conversation, cx),
3358 vec![
3359 (message_1.id, Role::User, 0..4),
3360 (message_3.id, Role::User, 4..5),
3361 (message_2.id, Role::User, 5..17),
3362 ]
3363 );
3364
3365 let (_, message_4) =
3366 conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx));
3367 let message_4 = message_4.unwrap();
3368 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3369 assert_eq!(
3370 messages(&conversation, cx),
3371 vec![
3372 (message_1.id, Role::User, 0..4),
3373 (message_3.id, Role::User, 4..5),
3374 (message_2.id, Role::User, 5..9),
3375 (message_4.id, Role::User, 9..17),
3376 ]
3377 );
3378
3379 let (_, message_5) =
3380 conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx));
3381 let message_5 = message_5.unwrap();
3382 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
3383 assert_eq!(
3384 messages(&conversation, cx),
3385 vec![
3386 (message_1.id, Role::User, 0..4),
3387 (message_3.id, Role::User, 4..5),
3388 (message_2.id, Role::User, 5..9),
3389 (message_4.id, Role::User, 9..10),
3390 (message_5.id, Role::User, 10..18),
3391 ]
3392 );
3393
3394 let (message_6, message_7) = conversation.update(cx, |conversation, cx| {
3395 conversation.split_message(14..16, cx)
3396 });
3397 let message_6 = message_6.unwrap();
3398 let message_7 = message_7.unwrap();
3399 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
3400 assert_eq!(
3401 messages(&conversation, cx),
3402 vec![
3403 (message_1.id, Role::User, 0..4),
3404 (message_3.id, Role::User, 4..5),
3405 (message_2.id, Role::User, 5..9),
3406 (message_4.id, Role::User, 9..10),
3407 (message_5.id, Role::User, 10..14),
3408 (message_6.id, Role::User, 14..17),
3409 (message_7.id, Role::User, 17..19),
3410 ]
3411 );
3412 }
3413
3414 #[gpui::test]
3415 fn test_messages_for_offsets(cx: &mut AppContext) {
3416 let settings_store = SettingsStore::test(cx);
3417 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3418 cx.set_global(settings_store);
3419 init(cx);
3420 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3421 let conversation =
3422 cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry, None, cx));
3423 let buffer = conversation.read(cx).buffer.clone();
3424
3425 let message_1 = conversation.read(cx).message_anchors[0].clone();
3426 assert_eq!(
3427 messages(&conversation, cx),
3428 vec![(message_1.id, Role::User, 0..0)]
3429 );
3430
3431 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
3432 let message_2 = conversation
3433 .update(cx, |conversation, cx| {
3434 conversation.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
3435 })
3436 .unwrap();
3437 buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
3438
3439 let message_3 = conversation
3440 .update(cx, |conversation, cx| {
3441 conversation.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3442 })
3443 .unwrap();
3444 buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
3445
3446 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
3447 assert_eq!(
3448 messages(&conversation, cx),
3449 vec![
3450 (message_1.id, Role::User, 0..4),
3451 (message_2.id, Role::User, 4..8),
3452 (message_3.id, Role::User, 8..11)
3453 ]
3454 );
3455
3456 assert_eq!(
3457 message_ids_for_offsets(&conversation, &[0, 4, 9], cx),
3458 [message_1.id, message_2.id, message_3.id]
3459 );
3460 assert_eq!(
3461 message_ids_for_offsets(&conversation, &[0, 1, 11], cx),
3462 [message_1.id, message_3.id]
3463 );
3464
3465 let message_4 = conversation
3466 .update(cx, |conversation, cx| {
3467 conversation.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
3468 })
3469 .unwrap();
3470 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n");
3471 assert_eq!(
3472 messages(&conversation, cx),
3473 vec![
3474 (message_1.id, Role::User, 0..4),
3475 (message_2.id, Role::User, 4..8),
3476 (message_3.id, Role::User, 8..12),
3477 (message_4.id, Role::User, 12..12)
3478 ]
3479 );
3480 assert_eq!(
3481 message_ids_for_offsets(&conversation, &[0, 4, 8, 12], cx),
3482 [message_1.id, message_2.id, message_3.id, message_4.id]
3483 );
3484
3485 fn message_ids_for_offsets(
3486 conversation: &Model<Conversation>,
3487 offsets: &[usize],
3488 cx: &AppContext,
3489 ) -> Vec<MessageId> {
3490 conversation
3491 .read(cx)
3492 .messages_for_offsets(offsets.iter().copied(), cx)
3493 .into_iter()
3494 .map(|message| message.id)
3495 .collect()
3496 }
3497 }
3498
3499 #[gpui::test]
3500 async fn test_serialization(cx: &mut TestAppContext) {
3501 let settings_store = cx.update(SettingsStore::test);
3502 cx.set_global(settings_store);
3503 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3504 cx.update(init);
3505 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
3506 let conversation = cx.new_model(|cx| {
3507 Conversation::new(LanguageModel::default(), registry.clone(), None, cx)
3508 });
3509 let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
3510 let message_0 =
3511 conversation.read_with(cx, |conversation, _| conversation.message_anchors[0].id);
3512 let message_1 = conversation.update(cx, |conversation, cx| {
3513 conversation
3514 .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
3515 .unwrap()
3516 });
3517 let message_2 = conversation.update(cx, |conversation, cx| {
3518 conversation
3519 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3520 .unwrap()
3521 });
3522 buffer.update(cx, |buffer, cx| {
3523 buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
3524 buffer.finalize_last_transaction();
3525 });
3526 let _message_3 = conversation.update(cx, |conversation, cx| {
3527 conversation
3528 .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
3529 .unwrap()
3530 });
3531 buffer.update(cx, |buffer, cx| buffer.undo(cx));
3532 assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
3533 assert_eq!(
3534 cx.read(|cx| messages(&conversation, cx)),
3535 [
3536 (message_0, Role::User, 0..2),
3537 (message_1.id, Role::Assistant, 2..6),
3538 (message_2.id, Role::System, 6..6),
3539 ]
3540 );
3541
3542 let deserialized_conversation = Conversation::deserialize(
3543 conversation.read_with(cx, |conversation, cx| conversation.serialize(cx)),
3544 LanguageModel::default(),
3545 Default::default(),
3546 registry.clone(),
3547 None,
3548 &mut cx.to_async(),
3549 )
3550 .await
3551 .unwrap();
3552 let deserialized_buffer =
3553 deserialized_conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
3554 assert_eq!(
3555 deserialized_buffer.read_with(cx, |buffer, _| buffer.text()),
3556 "a\nb\nc\n"
3557 );
3558 assert_eq!(
3559 cx.read(|cx| messages(&deserialized_conversation, cx)),
3560 [
3561 (message_0, Role::User, 0..2),
3562 (message_1.id, Role::Assistant, 2..6),
3563 (message_2.id, Role::System, 6..6),
3564 ]
3565 );
3566 }
3567
3568 fn messages(
3569 conversation: &Model<Conversation>,
3570 cx: &AppContext,
3571 ) -> Vec<(MessageId, Role, Range<usize>)> {
3572 conversation
3573 .read(cx)
3574 .messages(cx)
3575 .map(|message| (message.id, message.role, message.offset_range))
3576 .collect()
3577 }
3578}