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