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