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