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